diff --git a/api/cmd/bootstrap_test.go b/api/cmd/bootstrap_test.go index 6f7ba410..227c70cd 100644 --- a/api/cmd/bootstrap_test.go +++ b/api/cmd/bootstrap_test.go @@ -3,10 +3,11 @@ package cmd import ( "testing" - "github.com/caraml-dev/mlp/api/pkg/authz/enforcer" - enforcerMock "github.com/caraml-dev/mlp/api/pkg/authz/enforcer/mocks" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + + "github.com/caraml-dev/mlp/api/pkg/authz/enforcer" + enforcerMock "github.com/caraml-dev/mlp/api/pkg/authz/enforcer/mocks" ) func TestStartKetoBootsrap(t *testing.T) { diff --git a/api/config/config.go b/api/config/config.go index 9bb84c38..70257638 100644 --- a/api/config/config.go +++ b/api/config/config.go @@ -116,9 +116,12 @@ type UIConfig struct { StaticPath string `validated:"required"` IndexPath string `validated:"required"` - ClockworkUIHomepage string `json:"REACT_APP_CLOCKWORK_UI_HOMEPAGE"` - KubeflowUIHomepage string `json:"REACT_APP_KUBEFLOW_UI_HOMEPAGE"` - ProjectInfoUpdateEnabled bool `json:"REACT_APP_PROJECT_INFO_UPDATE_ENABLED"` + ClockworkUIHomepage string `json:"REACT_APP_CLOCKWORK_UI_HOMEPAGE"` + KubeflowUIHomepage string `json:"REACT_APP_KUBEFLOW_UI_HOMEPAGE"` + + AllowCustomStream bool `json:"REACT_APP_ALLOW_CUSTOM_STREAM"` + AllowCustomTeam bool `json:"REACT_APP_ALLOW_CUSTOM_TEAM"` + ProjectInfoUpdateEnabled bool `json:"REACT_APP_PROJECT_INFO_UPDATE_ENABLED"` } // Transform env variables to the format consumed by koanf. @@ -214,8 +217,10 @@ var defaultConfig = &Config{ TrackingURL: "", }, UI: &UIConfig{ - IndexPath: "index.html", - StaticPath: "ui/build", + IndexPath: "index.html", + StaticPath: "ui/build", + AllowCustomTeam: true, + AllowCustomStream: true, }, DefaultSecretStorage: &SecretStorage{ Name: "internal", diff --git a/api/config/config_test.go b/api/config/config_test.go index 0af09856..c6f2b59e 100644 --- a/api/config/config_test.go +++ b/api/config/config_test.go @@ -85,8 +85,10 @@ func TestLoad(t *testing.T) { "EmptyStream": {}, }, UI: &config.UIConfig{ - StaticPath: "ui/build", - IndexPath: "index.html", + StaticPath: "ui/build", + IndexPath: "index.html", + AllowCustomTeam: true, + AllowCustomStream: true, }, DefaultSecretStorage: &config.SecretStorage{ Name: "default-secret-storage", @@ -142,8 +144,10 @@ func TestLoad(t *testing.T) { "EmptyStream": {}, }, UI: &config.UIConfig{ - StaticPath: "ui/build", - IndexPath: "index.html", + StaticPath: "ui/build", + IndexPath: "index.html", + AllowCustomTeam: true, + AllowCustomStream: true, }, DefaultSecretStorage: &config.SecretStorage{ Name: "default-secret-storage", @@ -240,6 +244,8 @@ func TestLoad(t *testing.T) { ClockworkUIHomepage: "http://clockwork.dev", KubeflowUIHomepage: "http://kubeflow.org", + AllowCustomTeam: true, + AllowCustomStream: true, ProjectInfoUpdateEnabled: true, }, DefaultSecretStorage: &config.SecretStorage{ diff --git a/ui/packages/app/src/config.js b/ui/packages/app/src/config.js index a7151626..3ca8c158 100644 --- a/ui/packages/app/src/config.js +++ b/ui/packages/app/src/config.js @@ -34,6 +34,8 @@ const config = { CLOCKWORK_UI_HOMEPAGE: getEnv("REACT_APP_CLOCKWORK_UI_HOMEPAGE"), KUBEFLOW_UI_HOMEPAGE: getEnv("REACT_APP_KUBEFLOW_UI_HOMEPAGE"), + ALLOW_CUSTOM_STREAM: getEnv("REACT_APP_ALLOW_CUSTOM_STREAM") || true, + ALLOW_CUSTOM_TEAM: getEnv("REACT_APP_ALLOW_CUSTOM_TEAM") || true, PROJECT_INFO_UPDATE_ENABLED: getEnv("REACT_APP_PROJECT_INFO_UPDATE_ENABLED") || false }; diff --git a/ui/packages/app/src/project_setting/form/Labels.js b/ui/packages/app/src/project_setting/form/Labels.js index 660d83a2..d1b469d9 100644 --- a/ui/packages/app/src/project_setting/form/Labels.js +++ b/ui/packages/app/src/project_setting/form/Labels.js @@ -8,7 +8,7 @@ import { EuiButton, EuiFormRow } from "@elastic/eui"; -import { isDNS1123Label } from "../../validation/validation"; +import { isValidK8sLabelKeyValue } from "../../validation/validation"; export const Labels = ({ labels, @@ -52,7 +52,7 @@ export const Labels = ({ newItems[idx] = { ...newItems[idx], key: newKey, - isKeyValid: isDNS1123Label(newKey) + isKeyValid: isValidK8sLabelKeyValue(newKey) }; setItems(newItems); onChange(newItems); @@ -66,7 +66,7 @@ export const Labels = ({ newItems[idx] = { ...newItems[idx], value: newValue, - isValueValid: isDNS1123Label(newValue) + isValueValid: isValidK8sLabelKeyValue(newValue) }; setItems(newItems); onChange(newItems); diff --git a/ui/packages/app/src/project_setting/form/ProjectForm.js b/ui/packages/app/src/project_setting/form/ProjectForm.js index 82cfa37d..3685f913 100644 --- a/ui/packages/app/src/project_setting/form/ProjectForm.js +++ b/ui/packages/app/src/project_setting/form/ProjectForm.js @@ -13,9 +13,9 @@ import { addToast, useMlpApi } from "@caraml-dev/ui-lib"; import { ProjectFormContext } from "./context"; import { EmailTextArea } from "./EmailTextArea"; import { Labels } from "./Labels"; +import { isValidK8sLabelKeyValue } from "../../validation/validation"; import { Stream } from "./Stream"; import { Team } from "./Team"; -import { isDNS1123Label } from "../../validation/validation"; import { useNavigate } from "react-router-dom"; const ProjectForm = () => { @@ -35,7 +35,7 @@ const ProjectForm = () => { const [isValidProject, setIsValidProject] = useState(false); const onProjectChange = e => { const newValue = e.target.value; - let isValid = isDNS1123Label(newValue); + let isValid = isValidK8sLabelKeyValue(newValue); if (!isValid) { setProjectError( "Project name is invalid. It should contain only lowercase alphanumeric and dash ('-')" @@ -46,6 +46,7 @@ const ProjectForm = () => { }; const [isValidStream, setIsValidStream] = useState(false); + const [isValidTeam, setIsValidTeam] = useState(false); const onAdminValueChange = emails => { diff --git a/ui/packages/app/src/project_setting/form/Stream.js b/ui/packages/app/src/project_setting/form/Stream.js index e11dd707..18d3dc07 100644 --- a/ui/packages/app/src/project_setting/form/Stream.js +++ b/ui/packages/app/src/project_setting/form/Stream.js @@ -1,7 +1,7 @@ import React, { useState, useMemo } from "react"; import { EuiFormRow } from "@elastic/eui"; import { EuiComboBoxSelect } from "@caraml-dev/ui-lib"; -import { isDNS1123Label } from "../../validation/validation"; +import { isValidK8sLabelKeyValue } from "../../validation/validation"; import config from "../../config"; export const Stream = ({ @@ -21,10 +21,10 @@ export const Stream = ({ const [streamError, setStreamError] = useState(""); const onStreamChange = stream => { - let isValid = isDNS1123Label(stream); + let isValid = isValidK8sLabelKeyValue(stream); if (!isValid) { setStreamError( - "Stream name is invalid. It should contain only lowercase alphanumeric and dash (-), and must start and end with an alphanumeric character" + "Stream name is invalid. It should contain only lowercase alphanumeric and dash (-), or underscore (_) or period (.), and must start and end with an alphanumeric character" ); } setIsValidStream(isValid); @@ -37,8 +37,8 @@ export const Stream = ({ value={stream} options={streamOptions} onChange={onStreamChange} - onCreateOption={onStreamChange} - isDisabled={isDisabled} + onCreateOption={config.ALLOW_CUSTOM_STREAM ? onStreamChange : undefined} + isDiasbled={isDisabled} /> ); diff --git a/ui/packages/app/src/project_setting/form/Team.js b/ui/packages/app/src/project_setting/form/Team.js index 307c75a6..17a77014 100644 --- a/ui/packages/app/src/project_setting/form/Team.js +++ b/ui/packages/app/src/project_setting/form/Team.js @@ -1,7 +1,7 @@ import React, { useState, useEffect, useMemo } from "react"; import { EuiFormRow } from "@elastic/eui"; import { EuiComboBoxSelect } from "@caraml-dev/ui-lib"; -import { isDNS1123Label } from "../../validation/validation"; +import { isValidK8sLabelKeyValue } from "../../validation/validation"; import config from "../../config"; export const Team = ({ @@ -21,10 +21,10 @@ export const Team = ({ const [teamError, setTeamError] = useState(""); const onTeamChange = team => { - let isValid = isDNS1123Label(team); + let isValid = isValidK8sLabelKeyValue(team); if (!isValid) { setTeamError( - "Team name is invalid. It should contain only lowercase alphanumeric and dash (-), and must start and end with an alphanumeric character" + "Team name is invalid. It should contain only lowercase alphanumeric and dash (-) or underscore (_) or period (.), and must start and end with an alphanumeric character" ); } setIsValidTeam(isValid); @@ -43,8 +43,8 @@ export const Team = ({ value={team} options={teamOptions} onChange={onTeamChange} - onCreateOption={onTeamChange} - isDisabled={isDisabled} + onCreateOption={config.ALLOW_CUSTOM_TEAM ? onTeamChange : undefined} + isDiasbled={isDisabled} /> ); diff --git a/ui/packages/app/src/project_setting/project_info/ProjectInfoForm.js b/ui/packages/app/src/project_setting/project_info/ProjectInfoForm.js index 9c77433b..8a944468 100644 --- a/ui/packages/app/src/project_setting/project_info/ProjectInfoForm.js +++ b/ui/packages/app/src/project_setting/project_info/ProjectInfoForm.js @@ -9,7 +9,7 @@ import { } from "@elastic/eui"; import SubmitProjectInfoForm from "./SubmitProjectInfoForm"; import config from "../../config"; -import { isDNS1123Label } from "../../validation/validation"; +import { isValidK8sLabelKeyValue } from "../../validation/validation"; import { ProjectFormContext } from "../form/context"; import { Labels } from "../form/Labels"; import { Stream } from "../form/Stream"; @@ -21,9 +21,11 @@ const ProjectInfoForm = ({ originalProject, fetchUpdates }) => { ); const [isValidStream, setIsValidStream] = useState( - isDNS1123Label(project.stream) + isValidK8sLabelKeyValue(project.stream) + ); + const [isValidTeam, setIsValidTeam] = useState( + isValidK8sLabelKeyValue(project.team) ); - const [isValidTeam, setIsValidTeam] = useState(isDNS1123Label(project.team)); const [isValidLabels, setIsValidLabels] = useState( project.labels.length === 0 @@ -31,8 +33,8 @@ const ProjectInfoForm = ({ originalProject, fetchUpdates }) => { : project.labels.reduce((labelsValid, label) => { return ( labelsValid && - isDNS1123Label(label.key) && - isDNS1123Label(label.value) + isValidK8sLabelKeyValue(label.key) && + isValidK8sLabelKeyValue(label.value) ); }, true) ); diff --git a/ui/packages/app/src/validation/validation.js b/ui/packages/app/src/validation/validation.js index 6f298b1b..2259b571 100644 --- a/ui/packages/app/src/validation/validation.js +++ b/ui/packages/app/src/validation/validation.js @@ -1,7 +1,9 @@ // Test whether the value follow RFC1123 format const DNS1123LabelMaxLength = 63; -export const isDNS1123Label = value => { - const expression = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/; + +// See https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set for full details +export const isValidK8sLabelKeyValue = value => { + const expression = /^[a-z0-9]([_.\-a-z0-9]*[a-z0-9])?$/; if (value === undefined || value.length > DNS1123LabelMaxLength) { return false; }