From db672b885a5c1cdfc4a1893c26f5c4e422dff5bd Mon Sep 17 00:00:00 2001 From: Hung-Ting Wen Date: Tue, 1 Oct 2019 09:09:32 +0800 Subject: [PATCH] KfConfig - Internal data structure for holding config info (#4210) * KfctlConfig init * add more fields * add status and cache * link to gcp * add GetPluginSpec * add condition access funcs * add condition helper * add plugin access funcs * add plugin spec writer * remove functiosn from kfdef v1beta1 * remove kfloader * add sourceVersion * LoadConfigFromURI init * WriteConfigToFile init * copy applications * copy secrets * copy repos * copy plugins * add runtime fields * copy status * beta transformer * temp commit * updates * serialize plugins * serialize secrets * serialize repos * serialize cache * add interface * finish v1alpha1 * change naming * v1beta1 converter * test init * v1alpha1 test * updates * beta test init * finish test * fix * remove temp files * fix * KfConfig converter * appDir * write file * rename * fix test * remove temp codes * remove unused field * fix * add fields * fill in new fields in v1alpha1 * fix tests * fill fields in v1beta1 --- bootstrap/go.mod | 10 +- bootstrap/go.sum | 1 + .../apis/apps/configconverters/converters.go | 165 ++++++++ .../apps/configconverters/testdata/doc.go | 1 + .../testdata/kfconfig_v1alpha1.yaml | 333 ++++++++++++++++ .../testdata/kfconfig_v1beta1.yaml | 332 ++++++++++++++++ .../configconverters/testdata/v1alpha1.yaml | 334 ++++++++++++++++ .../configconverters/testdata/v1beta1.yaml | 332 ++++++++++++++++ .../apis/apps/configconverters/v1alpha1.go | 309 +++++++++++++++ .../apps/configconverters/v1alpha1_test.go | 132 +++++++ .../pkg/apis/apps/configconverters/v1beta1.go | 260 +++++++++++++ .../apps/configconverters/v1beta1_test.go | 60 +++ bootstrap/pkg/apis/apps/kfconfig/types.go | 362 ++++++++++++++++++ bootstrap/pkg/apis/apps/kfdef/expected.txt | 92 ----- bootstrap/pkg/apis/apps/kfdef/kfloader.go | 289 -------------- .../pkg/apis/apps/kfdef/kfloader_test.go | 128 ------- bootstrap/pkg/apis/apps/kfdef/read.txt | 92 ----- .../apps/kfdef/testdata/kfdef_v1alpha1.yaml | 49 --- .../apps/kfdef/testdata/kfdef_v1beta1.yaml | 50 --- .../apps/kfdef/v1beta1/application_types.go | 240 ------------ .../kfdef/v1beta1/application_types_test.go | 175 --------- 21 files changed, 2622 insertions(+), 1124 deletions(-) create mode 100644 bootstrap/pkg/apis/apps/configconverters/converters.go create mode 100644 bootstrap/pkg/apis/apps/configconverters/testdata/doc.go create mode 100644 bootstrap/pkg/apis/apps/configconverters/testdata/kfconfig_v1alpha1.yaml create mode 100644 bootstrap/pkg/apis/apps/configconverters/testdata/kfconfig_v1beta1.yaml create mode 100644 bootstrap/pkg/apis/apps/configconverters/testdata/v1alpha1.yaml create mode 100644 bootstrap/pkg/apis/apps/configconverters/testdata/v1beta1.yaml create mode 100644 bootstrap/pkg/apis/apps/configconverters/v1alpha1.go create mode 100644 bootstrap/pkg/apis/apps/configconverters/v1alpha1_test.go create mode 100644 bootstrap/pkg/apis/apps/configconverters/v1beta1.go create mode 100644 bootstrap/pkg/apis/apps/configconverters/v1beta1_test.go create mode 100644 bootstrap/pkg/apis/apps/kfconfig/types.go delete mode 100644 bootstrap/pkg/apis/apps/kfdef/expected.txt delete mode 100644 bootstrap/pkg/apis/apps/kfdef/kfloader.go delete mode 100644 bootstrap/pkg/apis/apps/kfdef/kfloader_test.go delete mode 100644 bootstrap/pkg/apis/apps/kfdef/read.txt delete mode 100644 bootstrap/pkg/apis/apps/kfdef/testdata/kfdef_v1alpha1.yaml delete mode 100644 bootstrap/pkg/apis/apps/kfdef/testdata/kfdef_v1beta1.yaml delete mode 100644 bootstrap/pkg/apis/apps/kfdef/v1beta1/application_types_test.go diff --git a/bootstrap/go.mod b/bootstrap/go.mod index 0f49df6eb43..4f1855d5527 100644 --- a/bootstrap/go.mod +++ b/bootstrap/go.mod @@ -7,14 +7,9 @@ require ( github.com/Sirupsen/logrus v0.0.0-00010101000000-000000000000 // indirect github.com/aws/aws-sdk-go v1.15.78 github.com/cenkalti/backoff v2.1.1+incompatible - github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 // indirect github.com/deckarep/golang-set v1.7.1 - github.com/docker/docker v1.13.1 // indirect - github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect github.com/elazarl/goproxy v0.0.0-20190711103511-473e67f1d7d2 // indirect github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 // indirect - github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect - github.com/fatih/camelcase v1.0.0 // indirect github.com/ghodss/yaml v1.0.0 github.com/go-kit/kit v0.8.0 github.com/gogo/protobuf v1.2.1 @@ -22,17 +17,14 @@ require ( github.com/golang/protobuf v1.3.1 github.com/hashicorp/go-getter v1.0.2 github.com/imdario/mergo v0.3.7 - github.com/jonboulle/clockwork v0.1.0 // indirect - github.com/kr/pty v1.1.3 // indirect + github.com/kubeflow/kfctl/v3 v3.0.0-20190917231916-6ebaf60b014a github.com/kubeflow/kubeflow/components/profile-controller/v2 v2.0.0 github.com/mitchellh/go-homedir v1.1.0 - github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/onrik/logrus v0.2.1 github.com/otiai10/copy v1.0.1 github.com/pkg/errors v0.8.1 github.com/prometheus/client_golang v0.9.2 github.com/prometheus/common v0.2.0 - github.com/russross/blackfriday v1.5.2 // indirect github.com/sirupsen/logrus v1.3.0 github.com/spf13/afero v1.2.2 github.com/spf13/cobra v0.0.3 diff --git a/bootstrap/go.sum b/bootstrap/go.sum index 4bfdc3323dd..eb994909036 100644 --- a/bootstrap/go.sum +++ b/bootstrap/go.sum @@ -534,6 +534,7 @@ k8s.io/utils v0.0.0-20190920012459-5008bf6f8cd6/go.mod h1:sZAwmy6armz5eXlNoLmJcl sigs.k8s.io/controller-runtime v0.1.12 h1:ovDq28E64PeY1yR+6H7DthakIC09soiDCrKvfP2tPYo= sigs.k8s.io/controller-runtime v0.1.12/go.mod h1:HFAYoOh6XMV+jKF1UjFwrknPbowfyHEHHRdJMf2jMX8= sigs.k8s.io/controller-tools v0.1.9/go.mod h1:6g08p9m9G/So3sBc1AOQifHfhxH/mb6Sc4z0LMI8XMw= +sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= sigs.k8s.io/kustomize/v3 v3.1.0 h1:FnNC1UtUjZlepvWUGwaAcFHw2rjNIaZvBUPCvaXz0Fo= sigs.k8s.io/kustomize/v3 v3.1.0/go.mod h1:ztX4zYc/QIww3gSripwF7TBOarBTm5BvyAMem0kCzOE= diff --git a/bootstrap/pkg/apis/apps/configconverters/converters.go b/bootstrap/pkg/apis/apps/configconverters/converters.go new file mode 100644 index 00000000000..28f3bd67dcc --- /dev/null +++ b/bootstrap/pkg/apis/apps/configconverters/converters.go @@ -0,0 +1,165 @@ +package configconverters + +import ( + "fmt" + "github.com/ghodss/yaml" + gogetter "github.com/hashicorp/go-getter" + kfapis "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis" + kfconfig "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis/apps/kfconfig" + log "github.com/sirupsen/logrus" + "io/ioutil" + netUrl "net/url" + "path" + "strings" +) + +type Converter interface { + ToKfConfig(appdir string, kfdefBytes []byte) (*kfconfig.KfConfig, error) + ToKfDefSerialized(config kfconfig.KfConfig) ([]byte, error) +} + +const ( + KfConfigFile = "app.yaml" + Api = "kfdef.apps.kubeflow.org" +) + +func isValidUrl(toTest string) bool { + _, err := netUrl.ParseRequestURI(toTest) + if err != nil { + return false + } else { + return true + } +} + +func LoadConfigFromURI(configFile string) (*kfconfig.KfConfig, error) { + if configFile == "" { + return nil, fmt.Errorf("config file must be the URI of a KfDef spec") + } + + // TODO(jlewi): We should check if configFile doesn't specify a protocol or the protocol + // is file:// then we can just read it rather than fetching with go-getter. + appDir, err := ioutil.TempDir("", "") + if err != nil { + return nil, fmt.Errorf("Create a temporary directory to copy the file to.") + } + // Open config file + // + // TODO(jlewi): Should we use hashicorp go-getter.GetAny here? We use that to download + // the tarballs for the repos. Maybe we should use that here as well to be consistent. + appFile := path.Join(appDir, KfConfigFile) + + log.Infof("Downloading %v to %v", configFile, appFile) + configFileUri, err := netUrl.Parse(configFile) + if err != nil { + log.Errorf("could not parse configFile url") + } + if isValidUrl(configFile) { + errGet := gogetter.GetFile(appFile, configFile) + if errGet != nil { + return nil, &kfapis.KfError{ + Code: int(kfapis.INVALID_ARGUMENT), + Message: fmt.Sprintf("could not fetch specified config %s: %v", configFile, err), + } + } + } else { + g := new(gogetter.FileGetter) + g.Copy = true + errGet := g.GetFile(appFile, configFileUri) + if errGet != nil { + return nil, &kfapis.KfError{ + Code: int(kfapis.INVALID_ARGUMENT), + Message: fmt.Sprintf("could not fetch specified config %s: %v", configFile, err), + } + } + } + + // Read contents + configFileBytes, err := ioutil.ReadFile(appFile) + if err != nil { + return nil, &kfapis.KfError{ + Code: int(kfapis.INTERNAL_ERROR), + Message: fmt.Sprintf("could not read from config file %s: %v", configFile, err), + } + } + + // Check API version. + var obj map[string]interface{} + if err = yaml.Unmarshal(configFileBytes, &obj); err != nil { + return nil, &kfapis.KfError{ + Code: int(kfapis.INVALID_ARGUMENT), + Message: fmt.Sprintf("invalid config file format: %v", err), + } + } + apiVersion, ok := obj["apiVersion"] + if !ok { + return nil, &kfapis.KfError{ + Code: int(kfapis.INVALID_ARGUMENT), + Message: "invalid config: apiVersion is not found.", + } + } + apiVersionSeparated := strings.Split(apiVersion.(string), "/") + if len(apiVersionSeparated) < 2 || apiVersionSeparated[0] != Api { + return nil, &kfapis.KfError{ + Code: int(kfapis.INVALID_ARGUMENT), + Message: fmt.Sprintf("invalid config: apiVersion must be in the format of %v/, got %v", Api, apiVersion), + } + } + + converters := map[string]Converter{ + "v1alpha1": V1alpha1{}, + "v1beta1": V1beta1{}, + } + + converter, ok := converters[apiVersionSeparated[1]] + if !ok { + versions := []string{} + for key := range converters { + versions = append(versions, key) + } + return nil, &kfapis.KfError{ + Code: int(kfapis.INVALID_ARGUMENT), + Message: fmt.Sprintf("invalid config: version not supported; supported versions: %v, got %v", + strings.Join(versions, ", "), apiVersionSeparated[1]), + } + } + return converter.ToKfConfig(appDir, configFileBytes) +} + +func WriteConfigToFile(config kfconfig.KfConfig, filename string) error { + converters := map[string]Converter{ + "v1alpha1": V1alpha1{}, + "v1beta1": V1beta1{}, + } + apiVersionSeparated := strings.Split(config.APIVersion, "/") + if len(apiVersionSeparated) < 2 || apiVersionSeparated[0] != Api { + return &kfapis.KfError{ + Code: int(kfapis.INVALID_ARGUMENT), + Message: fmt.Sprintf("invalid config: apiVersion must be in the format of %v/, got %v", Api, config.APIVersion), + } + } + + converter, ok := converters[apiVersionSeparated[1]] + if !ok { + return &kfapis.KfError{ + Code: int(kfapis.INVALID_ARGUMENT), + Message: fmt.Sprintf("invalid config: unable to find converter for version %v", config.APIVersion), + } + } + + kfdefBytes, err := converter.ToKfDefSerialized(config) + if err != nil { + return &kfapis.KfError{ + Code: int(kfapis.INVALID_ARGUMENT), + Message: fmt.Sprintf("error when marshaling KfDef: %v", err), + } + } + err = ioutil.WriteFile(filename, kfdefBytes, 0644) + if err != nil { + return &kfapis.KfError{ + Code: int(kfapis.INTERNAL_ERROR), + Message: fmt.Sprintf("error when writing KfDef: %v", err), + } + } + return nil +} diff --git a/bootstrap/pkg/apis/apps/configconverters/testdata/doc.go b/bootstrap/pkg/apis/apps/configconverters/testdata/doc.go new file mode 100644 index 00000000000..69d29d3c6c6 --- /dev/null +++ b/bootstrap/pkg/apis/apps/configconverters/testdata/doc.go @@ -0,0 +1 @@ +package testdata diff --git a/bootstrap/pkg/apis/apps/configconverters/testdata/kfconfig_v1alpha1.yaml b/bootstrap/pkg/apis/apps/configconverters/testdata/kfconfig_v1alpha1.yaml new file mode 100644 index 00000000000..5bc0eb36129 --- /dev/null +++ b/bootstrap/pkg/apis/apps/configconverters/testdata/kfconfig_v1alpha1.yaml @@ -0,0 +1,333 @@ +apiVersion: kfdef.apps.kubeflow.org/v1alpha1 +appDir: /tmp/myapp2 +project: foo-project +email: foo@gmail.com +applications: +- kustomizeConfig: + parameters: + - name: namespace + value: istio-system + repoRef: + name: manifests + path: istio/istio-crds + name: istio-crds +- kustomizeConfig: + parameters: + - name: namespace + value: istio-system + repoRef: + name: manifests + path: istio/istio-install + name: istio-install +- kustomizeConfig: + parameters: + - name: clusterRbacConfig + value: "ON" + repoRef: + name: manifests + path: istio/istio + name: istio +- kustomizeConfig: + repoRef: + name: manifests + path: application/application-crds + name: application-crds +- kustomizeConfig: + overlays: + - application + repoRef: + name: manifests + path: application/application + name: application +- kustomizeConfig: + repoRef: + name: manifests + path: metacontroller + name: metacontroller +- kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: argo + name: argo +- kustomizeConfig: + overlays: + - istio + parameters: + - name: userid-header + value: X-Goog-Authenticated-User-Email + - name: userid-prefix + value: 'accounts.google.com:' + repoRef: + name: manifests + path: common/centraldashboard + name: centraldashboard +- kustomizeConfig: + repoRef: + name: manifests + path: admission-webhook/webhook + name: webhook +- kustomizeConfig: + parameters: + - name: webhookNamePrefix + value: admission-webhook- + repoRef: + name: manifests + path: admission-webhook/bootstrap + name: bootstrap +- kustomizeConfig: + overlays: + - istio + - application + parameters: + - name: userid-header + value: X-Goog-Authenticated-User-Email + - name: userid-prefix + value: 'accounts.google.com:' + repoRef: + name: manifests + path: jupyter/jupyter-web-app + name: jupyter-web-app +- kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/katib-db + name: katib-db +- kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/katib-manager + name: katib-manager +- kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/katib-controller + name: katib-controller +- kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: katib-v1alpha2/katib-ui + name: katib-ui +- kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: metadata + name: metadata +- kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/metrics-collector + name: metrics-collector +- kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/suggestion + name: suggestion +- kustomizeConfig: + overlays: + - istio + - application + parameters: + - name: injectGcpCredentials + value: "true" + repoRef: + name: manifests + path: jupyter/notebook-controller + name: notebook-controller +- kustomizeConfig: + repoRef: + name: manifests + path: pytorch-job/pytorch-job-crds + name: pytorch-job-crds +- kustomizeConfig: + overlays: + - application + repoRef: + name: manifests + path: pytorch-job/pytorch-operator + name: pytorch-operator +- kustomizeConfig: + parameters: + - name: namespace + value: knative-serving + repoRef: + name: manifests + path: knative/knative-serving-crds + name: knative-crds +- kustomizeConfig: + parameters: + - name: namespace + value: knative-serving + repoRef: + name: manifests + path: knative/knative-serving-install + name: knative-install +- kustomizeConfig: + repoRef: + name: manifests + path: kfserving/kfserving-crds + name: kfserving-crds +- kustomizeConfig: + repoRef: + name: manifests + path: kfserving/kfserving-install + name: kfserving-install +- kustomizeConfig: + parameters: + - name: usageId + value: "123456" + - name: reportUsage + value: "true" + repoRef: + name: manifests + path: common/spartakus + name: spartakus +- kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: tensorboard + name: tensorboard +- kustomizeConfig: + overlays: + - istio + - application + repoRef: + name: manifests + path: tf-training/tf-job-operator + name: tf-job-operator +- kustomizeConfig: + repoRef: + name: manifests + path: pipeline/api-service + name: api-service +- kustomizeConfig: + overlays: + - minioPd + parameters: + - name: minioPd + value: test1-storage-artifact-store + - name: minioPvName + value: minio-pv + - name: minioPvcName + value: minio-pv-claim + repoRef: + name: manifests + path: pipeline/minio + name: minio +- kustomizeConfig: + overlays: + - mysqlPd + parameters: + - name: mysqlPd + value: test1-storage-metadata-store + - name: mysqlPvName + value: mysql-pv + - name: mysqlPvcName + value: mysql-pv-claim + repoRef: + name: manifests + path: pipeline/mysql + name: mysql +- kustomizeConfig: + repoRef: + name: manifests + path: pipeline/persistent-agent + name: persistent-agent +- kustomizeConfig: + repoRef: + name: manifests + path: pipeline/pipelines-runner + name: pipelines-runner +- kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: pipeline/pipelines-ui + name: pipelines-ui +- kustomizeConfig: + repoRef: + name: manifests + path: pipeline/pipelines-viewer + name: pipelines-viewer +- kustomizeConfig: + repoRef: + name: manifests + path: pipeline/scheduledworkflow + name: scheduledworkflow +- kustomizeConfig: + repoRef: + name: manifests + path: gcp/cloud-endpoints + name: cloud-endpoints +- kustomizeConfig: + overlays: + - istio + parameters: + - name: admin + value: SET_EMAIL + - name: userid-header + value: X-Goog-Authenticated-User-Email + - name: userid-prefix + value: 'accounts.google.com:' + repoRef: + name: manifests + path: profiles + name: profiles +- kustomizeConfig: + repoRef: + name: manifests + path: gcp/gpu-driver + name: gpu-driver +- kustomizeConfig: + overlays: + - managed-cert + parameters: + - name: namespace + value: istio-system + - name: ipName + value: myapp2-ip + - name: hostname + value: myapp2.endpoints.foo-project.cloud.goog + repoRef: + name: manifests + path: gcp/iap-ingress + name: iap-ingress +- kustomizeConfig: + overlays: + - application + repoRef: + name: manifests + path: seldon/seldon-core-operator + name: seldon-core-operator +kind: KfConfig +metadata: + creationTimestamp: null + name: myapp2 + namespace: kubeflow +plugins: +- kind: KfGcpPlugin + name: gcp + namespace: kubeflow + spec: + createPipelinePersistentStorage: true + deploymentManagerConfig: + repoRef: + name: manifests + path: gcp/deployment_manager_configs + email: foo@gmail.com + enableWorkloadIdentity: true + project: foo-project + skipInitProject: true + useBasicAuth: false +repos: +- name: manifests + uri: https://github.com/kubeflow/manifests/archive/master.tar.gz +status: {} diff --git a/bootstrap/pkg/apis/apps/configconverters/testdata/kfconfig_v1beta1.yaml b/bootstrap/pkg/apis/apps/configconverters/testdata/kfconfig_v1beta1.yaml new file mode 100644 index 00000000000..45d43ec2ddb --- /dev/null +++ b/bootstrap/pkg/apis/apps/configconverters/testdata/kfconfig_v1beta1.yaml @@ -0,0 +1,332 @@ +apiVersion: kfdef.apps.kubeflow.org/v1beta1 +kind: KfConfig +project: foo-project +email: foo@gmail.com +applications: +- kustomizeConfig: + parameters: + - name: namespace + value: istio-system + repoRef: + name: manifests + path: istio/istio-crds + name: istio-crds +- kustomizeConfig: + parameters: + - name: namespace + value: istio-system + repoRef: + name: manifests + path: istio/istio-install + name: istio-install +- kustomizeConfig: + parameters: + - name: clusterRbacConfig + value: "ON" + repoRef: + name: manifests + path: istio/istio + name: istio +- kustomizeConfig: + repoRef: + name: manifests + path: application/application-crds + name: application-crds +- kustomizeConfig: + overlays: + - application + repoRef: + name: manifests + path: application/application + name: application +- kustomizeConfig: + repoRef: + name: manifests + path: metacontroller + name: metacontroller +- kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: argo + name: argo +- kustomizeConfig: + overlays: + - istio + parameters: + - name: userid-header + value: X-Goog-Authenticated-User-Email + - name: userid-prefix + value: 'accounts.google.com:' + repoRef: + name: manifests + path: common/centraldashboard + name: centraldashboard +- kustomizeConfig: + repoRef: + name: manifests + path: admission-webhook/webhook + name: webhook +- kustomizeConfig: + parameters: + - name: webhookNamePrefix + value: admission-webhook- + repoRef: + name: manifests + path: admission-webhook/bootstrap + name: bootstrap +- kustomizeConfig: + overlays: + - istio + - application + parameters: + - name: userid-header + value: X-Goog-Authenticated-User-Email + - name: userid-prefix + value: 'accounts.google.com:' + repoRef: + name: manifests + path: jupyter/jupyter-web-app + name: jupyter-web-app +- kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/katib-db + name: katib-db +- kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/katib-manager + name: katib-manager +- kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/katib-controller + name: katib-controller +- kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: katib-v1alpha2/katib-ui + name: katib-ui +- kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: metadata + name: metadata +- kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/metrics-collector + name: metrics-collector +- kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/suggestion + name: suggestion +- kustomizeConfig: + overlays: + - istio + - application + parameters: + - name: injectGcpCredentials + value: "true" + repoRef: + name: manifests + path: jupyter/notebook-controller + name: notebook-controller +- kustomizeConfig: + repoRef: + name: manifests + path: pytorch-job/pytorch-job-crds + name: pytorch-job-crds +- kustomizeConfig: + overlays: + - application + repoRef: + name: manifests + path: pytorch-job/pytorch-operator + name: pytorch-operator +- kustomizeConfig: + parameters: + - name: namespace + value: knative-serving + repoRef: + name: manifests + path: knative/knative-serving-crds + name: knative-crds +- kustomizeConfig: + parameters: + - name: namespace + value: knative-serving + repoRef: + name: manifests + path: knative/knative-serving-install + name: knative-install +- kustomizeConfig: + repoRef: + name: manifests + path: kfserving/kfserving-crds + name: kfserving-crds +- kustomizeConfig: + repoRef: + name: manifests + path: kfserving/kfserving-install + name: kfserving-install +- kustomizeConfig: + parameters: + - name: usageId + value: "123456" + - name: reportUsage + value: "true" + repoRef: + name: manifests + path: common/spartakus + name: spartakus +- kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: tensorboard + name: tensorboard +- kustomizeConfig: + overlays: + - istio + - application + repoRef: + name: manifests + path: tf-training/tf-job-operator + name: tf-job-operator +- kustomizeConfig: + repoRef: + name: manifests + path: pipeline/api-service + name: api-service +- kustomizeConfig: + overlays: + - minioPd + parameters: + - name: minioPd + value: test1-storage-artifact-store + - name: minioPvName + value: minio-pv + - name: minioPvcName + value: minio-pv-claim + repoRef: + name: manifests + path: pipeline/minio + name: minio +- kustomizeConfig: + overlays: + - mysqlPd + parameters: + - name: mysqlPd + value: test1-storage-metadata-store + - name: mysqlPvName + value: mysql-pv + - name: mysqlPvcName + value: mysql-pv-claim + repoRef: + name: manifests + path: pipeline/mysql + name: mysql +- kustomizeConfig: + repoRef: + name: manifests + path: pipeline/persistent-agent + name: persistent-agent +- kustomizeConfig: + repoRef: + name: manifests + path: pipeline/pipelines-runner + name: pipelines-runner +- kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: pipeline/pipelines-ui + name: pipelines-ui +- kustomizeConfig: + repoRef: + name: manifests + path: pipeline/pipelines-viewer + name: pipelines-viewer +- kustomizeConfig: + repoRef: + name: manifests + path: pipeline/scheduledworkflow + name: scheduledworkflow +- kustomizeConfig: + repoRef: + name: manifests + path: gcp/cloud-endpoints + name: cloud-endpoints +- kustomizeConfig: + overlays: + - istio + parameters: + - name: admin + value: SET_EMAIL + - name: userid-header + value: X-Goog-Authenticated-User-Email + - name: userid-prefix + value: 'accounts.google.com:' + repoRef: + name: manifests + path: profiles + name: profiles +- kustomizeConfig: + repoRef: + name: manifests + path: gcp/gpu-driver + name: gpu-driver +- kustomizeConfig: + overlays: + - managed-cert + parameters: + - name: namespace + value: istio-system + - name: ipName + value: myapp2-ip + - name: hostname + value: myapp2.endpoints.foo-project.cloud.goog + repoRef: + name: manifests + path: gcp/iap-ingress + name: iap-ingress +- kustomizeConfig: + overlays: + - application + repoRef: + name: manifests + path: seldon/seldon-core-operator + name: seldon-core-operator +metadata: + creationTimestamp: null + name: myapp2 + namespace: kubeflow +plugins: +- kind: KfGcpPlugin + name: gcp + namespace: kubeflow + spec: + createPipelinePersistentStorage: true + deploymentManagerConfig: + repoRef: + name: manifests + path: gcp/deployment_manager_configs + email: foo@gmail.com + enableWorkloadIdentity: true + project: foo-project + skipInitProject: true + useBasicAuth: false +repos: +- name: manifests + uri: https://github.com/kubeflow/manifests/archive/master.tar.gz +status: {} diff --git a/bootstrap/pkg/apis/apps/configconverters/testdata/v1alpha1.yaml b/bootstrap/pkg/apis/apps/configconverters/testdata/v1alpha1.yaml new file mode 100644 index 00000000000..56ec024059e --- /dev/null +++ b/bootstrap/pkg/apis/apps/configconverters/testdata/v1alpha1.yaml @@ -0,0 +1,334 @@ +apiVersion: kfdef.apps.kubeflow.org/v1alpha1 +kind: KfDef +metadata: + creationTimestamp: null + name: myapp2 + namespace: kubeflow +spec: + repos: + - name: manifests + uri: https://github.com/kubeflow/manifests/archive/master.tar.gz + appdir: /tmp/myapp2 + applications: + - kustomizeConfig: + parameters: + - name: namespace + value: istio-system + repoRef: + name: manifests + path: istio/istio-crds + name: istio-crds + - kustomizeConfig: + parameters: + - name: namespace + value: istio-system + repoRef: + name: manifests + path: istio/istio-install + name: istio-install + - kustomizeConfig: + parameters: + - name: clusterRbacConfig + value: "ON" + repoRef: + name: manifests + path: istio/istio + name: istio + - kustomizeConfig: + repoRef: + name: manifests + path: application/application-crds + name: application-crds + - kustomizeConfig: + overlays: + - application + repoRef: + name: manifests + path: application/application + name: application + - kustomizeConfig: + repoRef: + name: manifests + path: metacontroller + name: metacontroller + - kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: argo + name: argo + - kustomizeConfig: + overlays: + - istio + parameters: + - name: userid-header + value: X-Goog-Authenticated-User-Email + - name: userid-prefix + value: "accounts.google.com:" + repoRef: + name: manifests + path: common/centraldashboard + name: centraldashboard + - kustomizeConfig: + repoRef: + name: manifests + path: admission-webhook/webhook + name: webhook + - kustomizeConfig: + parameters: + - name: webhookNamePrefix + value: admission-webhook- + repoRef: + name: manifests + path: admission-webhook/bootstrap + name: bootstrap + - kustomizeConfig: + overlays: + - istio + - application + parameters: + - name: userid-header + value: X-Goog-Authenticated-User-Email + - name: userid-prefix + value: "accounts.google.com:" + repoRef: + name: manifests + path: jupyter/jupyter-web-app + name: jupyter-web-app + - kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/katib-db + name: katib-db + - kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/katib-manager + name: katib-manager + - kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/katib-controller + name: katib-controller + - kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: katib-v1alpha2/katib-ui + name: katib-ui + - kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: metadata + name: metadata + - kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/metrics-collector + name: metrics-collector + - kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/suggestion + name: suggestion + - kustomizeConfig: + overlays: + - istio + - application + parameters: + - name: injectGcpCredentials + value: "true" + repoRef: + name: manifests + path: jupyter/notebook-controller + name: notebook-controller + - kustomizeConfig: + repoRef: + name: manifests + path: pytorch-job/pytorch-job-crds + name: pytorch-job-crds + - kustomizeConfig: + overlays: + - application + repoRef: + name: manifests + path: pytorch-job/pytorch-operator + name: pytorch-operator + - kustomizeConfig: + parameters: + - name: namespace + value: knative-serving + repoRef: + name: manifests + path: knative/knative-serving-crds + name: knative-crds + - kustomizeConfig: + parameters: + - name: namespace + value: knative-serving + repoRef: + name: manifests + path: knative/knative-serving-install + name: knative-install + - kustomizeConfig: + repoRef: + name: manifests + path: kfserving/kfserving-crds + name: kfserving-crds + - kustomizeConfig: + repoRef: + name: manifests + path: kfserving/kfserving-install + name: kfserving-install + - kustomizeConfig: + parameters: + - name: usageId + value: "123456" + - name: reportUsage + value: "true" + repoRef: + name: manifests + path: common/spartakus + name: spartakus + - kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: tensorboard + name: tensorboard + - kustomizeConfig: + overlays: + - istio + - application + repoRef: + name: manifests + path: tf-training/tf-job-operator + name: tf-job-operator + - kustomizeConfig: + repoRef: + name: manifests + path: pipeline/api-service + name: api-service + - kustomizeConfig: + overlays: + - minioPd + parameters: + - name: minioPd + value: test1-storage-artifact-store + - name: minioPvName + value: minio-pv + - name: minioPvcName + value: minio-pv-claim + repoRef: + name: manifests + path: pipeline/minio + name: minio + - kustomizeConfig: + overlays: + - mysqlPd + parameters: + - name: mysqlPd + value: test1-storage-metadata-store + - name: mysqlPvName + value: mysql-pv + - name: mysqlPvcName + value: mysql-pv-claim + repoRef: + name: manifests + path: pipeline/mysql + name: mysql + - kustomizeConfig: + repoRef: + name: manifests + path: pipeline/persistent-agent + name: persistent-agent + - kustomizeConfig: + repoRef: + name: manifests + path: pipeline/pipelines-runner + name: pipelines-runner + - kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: pipeline/pipelines-ui + name: pipelines-ui + - kustomizeConfig: + repoRef: + name: manifests + path: pipeline/pipelines-viewer + name: pipelines-viewer + - kustomizeConfig: + repoRef: + name: manifests + path: pipeline/scheduledworkflow + name: scheduledworkflow + - kustomizeConfig: + repoRef: + name: manifests + path: gcp/cloud-endpoints + name: cloud-endpoints + - kustomizeConfig: + overlays: + - istio + parameters: + - name: admin + value: SET_EMAIL + - name: userid-header + value: X-Goog-Authenticated-User-Email + - name: userid-prefix + value: "accounts.google.com:" + repoRef: + name: manifests + path: profiles + name: profiles + - kustomizeConfig: + repoRef: + name: manifests + path: gcp/gpu-driver + name: gpu-driver + - kustomizeConfig: + overlays: + - managed-cert + parameters: + - name: namespace + value: istio-system + - name: ipName + value: myapp2-ip + - name: hostname + value: myapp2.endpoints.foo-project.cloud.goog + repoRef: + name: manifests + path: gcp/iap-ingress + name: iap-ingress + - kustomizeConfig: + overlays: + - application + repoRef: + name: manifests + path: seldon/seldon-core-operator + name: seldon-core-operator + email: foo@gmail.com + enableApplications: true + packageManager: kustomize + platform: gcp + plugins: + - name: gcp + spec: + createPipelinePersistentStorage: true + deploymentManagerConfig: + repoRef: + name: manifests + path: gcp/deployment_manager_configs + enableWorkloadIdentity: true + useBasicAuth: false + skipInitProject: true + useBasicAuth: false + useIstio: true + project: foo-project diff --git a/bootstrap/pkg/apis/apps/configconverters/testdata/v1beta1.yaml b/bootstrap/pkg/apis/apps/configconverters/testdata/v1beta1.yaml new file mode 100644 index 00000000000..aa1f1098b75 --- /dev/null +++ b/bootstrap/pkg/apis/apps/configconverters/testdata/v1beta1.yaml @@ -0,0 +1,332 @@ +apiVersion: kfdef.apps.kubeflow.org/v1beta1 +kind: KfDef +metadata: + creationTimestamp: null + name: myapp2 + namespace: kubeflow +spec: + applications: + - kustomizeConfig: + parameters: + - name: namespace + value: istio-system + repoRef: + name: manifests + path: istio/istio-crds + name: istio-crds + - kustomizeConfig: + parameters: + - name: namespace + value: istio-system + repoRef: + name: manifests + path: istio/istio-install + name: istio-install + - kustomizeConfig: + parameters: + - name: clusterRbacConfig + value: "ON" + repoRef: + name: manifests + path: istio/istio + name: istio + - kustomizeConfig: + repoRef: + name: manifests + path: application/application-crds + name: application-crds + - kustomizeConfig: + overlays: + - application + repoRef: + name: manifests + path: application/application + name: application + - kustomizeConfig: + repoRef: + name: manifests + path: metacontroller + name: metacontroller + - kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: argo + name: argo + - kustomizeConfig: + overlays: + - istio + parameters: + - name: userid-header + value: X-Goog-Authenticated-User-Email + - name: userid-prefix + value: 'accounts.google.com:' + repoRef: + name: manifests + path: common/centraldashboard + name: centraldashboard + - kustomizeConfig: + repoRef: + name: manifests + path: admission-webhook/webhook + name: webhook + - kustomizeConfig: + parameters: + - name: webhookNamePrefix + value: admission-webhook- + repoRef: + name: manifests + path: admission-webhook/bootstrap + name: bootstrap + - kustomizeConfig: + overlays: + - istio + - application + parameters: + - name: userid-header + value: X-Goog-Authenticated-User-Email + - name: userid-prefix + value: 'accounts.google.com:' + repoRef: + name: manifests + path: jupyter/jupyter-web-app + name: jupyter-web-app + - kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/katib-db + name: katib-db + - kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/katib-manager + name: katib-manager + - kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/katib-controller + name: katib-controller + - kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: katib-v1alpha2/katib-ui + name: katib-ui + - kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: metadata + name: metadata + - kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/metrics-collector + name: metrics-collector + - kustomizeConfig: + repoRef: + name: manifests + path: katib-v1alpha2/suggestion + name: suggestion + - kustomizeConfig: + overlays: + - istio + - application + parameters: + - name: injectGcpCredentials + value: "true" + repoRef: + name: manifests + path: jupyter/notebook-controller + name: notebook-controller + - kustomizeConfig: + repoRef: + name: manifests + path: pytorch-job/pytorch-job-crds + name: pytorch-job-crds + - kustomizeConfig: + overlays: + - application + repoRef: + name: manifests + path: pytorch-job/pytorch-operator + name: pytorch-operator + - kustomizeConfig: + parameters: + - name: namespace + value: knative-serving + repoRef: + name: manifests + path: knative/knative-serving-crds + name: knative-crds + - kustomizeConfig: + parameters: + - name: namespace + value: knative-serving + repoRef: + name: manifests + path: knative/knative-serving-install + name: knative-install + - kustomizeConfig: + repoRef: + name: manifests + path: kfserving/kfserving-crds + name: kfserving-crds + - kustomizeConfig: + repoRef: + name: manifests + path: kfserving/kfserving-install + name: kfserving-install + - kustomizeConfig: + parameters: + - name: usageId + value: "123456" + - name: reportUsage + value: "true" + repoRef: + name: manifests + path: common/spartakus + name: spartakus + - kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: tensorboard + name: tensorboard + - kustomizeConfig: + overlays: + - istio + - application + repoRef: + name: manifests + path: tf-training/tf-job-operator + name: tf-job-operator + - kustomizeConfig: + repoRef: + name: manifests + path: pipeline/api-service + name: api-service + - kustomizeConfig: + overlays: + - minioPd + parameters: + - name: minioPd + value: test1-storage-artifact-store + - name: minioPvName + value: minio-pv + - name: minioPvcName + value: minio-pv-claim + repoRef: + name: manifests + path: pipeline/minio + name: minio + - kustomizeConfig: + overlays: + - mysqlPd + parameters: + - name: mysqlPd + value: test1-storage-metadata-store + - name: mysqlPvName + value: mysql-pv + - name: mysqlPvcName + value: mysql-pv-claim + repoRef: + name: manifests + path: pipeline/mysql + name: mysql + - kustomizeConfig: + repoRef: + name: manifests + path: pipeline/persistent-agent + name: persistent-agent + - kustomizeConfig: + repoRef: + name: manifests + path: pipeline/pipelines-runner + name: pipelines-runner + - kustomizeConfig: + overlays: + - istio + repoRef: + name: manifests + path: pipeline/pipelines-ui + name: pipelines-ui + - kustomizeConfig: + repoRef: + name: manifests + path: pipeline/pipelines-viewer + name: pipelines-viewer + - kustomizeConfig: + repoRef: + name: manifests + path: pipeline/scheduledworkflow + name: scheduledworkflow + - kustomizeConfig: + repoRef: + name: manifests + path: gcp/cloud-endpoints + name: cloud-endpoints + - kustomizeConfig: + overlays: + - istio + parameters: + - name: admin + value: SET_EMAIL + - name: userid-header + value: X-Goog-Authenticated-User-Email + - name: userid-prefix + value: 'accounts.google.com:' + repoRef: + name: manifests + path: profiles + name: profiles + - kustomizeConfig: + repoRef: + name: manifests + path: gcp/gpu-driver + name: gpu-driver + - kustomizeConfig: + overlays: + - managed-cert + parameters: + - name: namespace + value: istio-system + - name: ipName + value: myapp2-ip + - name: hostname + value: myapp2.endpoints.foo-project.cloud.goog + repoRef: + name: manifests + path: gcp/iap-ingress + name: iap-ingress + - kustomizeConfig: + overlays: + - application + repoRef: + name: manifests + path: seldon/seldon-core-operator + name: seldon-core-operator + plugins: + - kind: KfGcpPlugin + metadata: + creationTimestamp: null + name: gcp + spec: + createPipelinePersistentStorage: true + deploymentManagerConfig: + repoRef: + name: manifests + path: gcp/deployment_manager_configs + email: foo@gmail.com + enableWorkloadIdentity: true + project: foo-project + skipInitProject: true + useBasicAuth: false + repos: + - name: manifests + uri: https://github.com/kubeflow/manifests/archive/master.tar.gz +status: {} diff --git a/bootstrap/pkg/apis/apps/configconverters/v1alpha1.go b/bootstrap/pkg/apis/apps/configconverters/v1alpha1.go new file mode 100644 index 00000000000..34fdff602e6 --- /dev/null +++ b/bootstrap/pkg/apis/apps/configconverters/v1alpha1.go @@ -0,0 +1,309 @@ +package configconverters + +import ( + "fmt" + "github.com/ghodss/yaml" + configsv3 "github.com/kubeflow/kubeflow/bootstrap/v3/config" + kfapis "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis" + kftypesv3 "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis/apps" + kfconfig "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis/apps/kfconfig" + kfdeftypes "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis/apps/kfdef/v1alpha1" + kfgcp "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/kfapp/gcp" +) + +// Empty struct - used to implement Converter interface. +type V1alpha1 struct { +} + +func pluginNameToKind(pluginName string) kfconfig.PluginKindType { + mapper := map[string]kfconfig.PluginKindType{ + kftypesv3.AWS: kfconfig.AWS_PLUGIN_KIND, + kftypesv3.GCP: kfconfig.GCP_PLUGIN_KIND, + kftypesv3.MINIKUBE: kfconfig.MINIKUBE_PLUGIN_KIND, + kftypesv3.EXISTING_ARRIKTO: kfconfig.EXISTING_ARRIKTO_PLUGIN_KIND, + } + kind, ok := mapper[pluginName] + if ok { + return kind + } else { + return kfconfig.PluginKindType("KfUnknownPlugin") + } +} + +// Copy GCP plugin spec. Will skip if platform is not GCP. +func copyGcpPluginSpec(from *kfdeftypes.KfDef, to *kfconfig.KfConfig) error { + if from.Spec.Platform != kftypesv3.GCP { + return nil + } + + spec := kfgcp.GcpPluginSpec{} + if err := to.GetPluginSpec(kfconfig.GCP_PLUGIN_KIND, &spec); err != nil && !kfconfig.IsPluginNotFound(err) { + return err + } + spec.Project = from.Spec.Project + spec.Email = from.Spec.Email + spec.IpName = from.Spec.IpName + spec.Hostname = from.Spec.Hostname + spec.Zone = from.Spec.Zone + spec.UseBasicAuth = from.Spec.UseBasicAuth + spec.SkipInitProject = from.Spec.SkipInitProject + spec.DeleteStorage = from.Spec.DeleteStorage + return to.SetPluginSpec(kfconfig.GCP_PLUGIN_KIND, spec) +} + +func (v V1alpha1) ToKfConfig(appdir string, kfdefBytes []byte) (*kfconfig.KfConfig, error) { + kfdef := &kfdeftypes.KfDef{} + if err := yaml.Unmarshal(kfdefBytes, kfdef); err != nil { + return nil, &kfapis.KfError{ + Code: int(kfapis.INTERNAL_ERROR), + Message: fmt.Sprintf("could not unmarshal config file onto KfDef struct: %v", err), + } + } + + config := &kfconfig.KfConfig{ + AppDir: kfdef.Spec.AppDir, + UseBasicAuth: kfdef.Spec.UseBasicAuth, + Project: kfdef.Spec.Project, + Email: kfdef.Spec.Email, + IpName: kfdef.Spec.IpName, + Hostname: kfdef.Spec.Hostname, + Zone: kfdef.Spec.Zone, + } + if config.AppDir == "" { + config.AppDir = appdir + } + config.Name = kfdef.Name + config.Namespace = kfdef.Namespace + config.APIVersion = kfdef.APIVersion + config.Kind = "KfConfig" + for _, app := range kfdef.Spec.Applications { + application := kfconfig.Application{ + Name: app.Name, + } + if app.KustomizeConfig != nil { + kconfig := &kfconfig.KustomizeConfig{ + Overlays: app.KustomizeConfig.Overlays, + } + if app.KustomizeConfig.RepoRef != nil { + kref := &kfconfig.RepoRef{ + Name: app.KustomizeConfig.RepoRef.Name, + Path: app.KustomizeConfig.RepoRef.Path, + } + kconfig.RepoRef = kref + } + for _, param := range app.KustomizeConfig.Parameters { + p := kfconfig.NameValue{ + Name: param.Name, + Value: param.Value, + } + kconfig.Parameters = append(kconfig.Parameters, p) + } + application.KustomizeConfig = kconfig + } + config.Applications = append(config.Applications, application) + } + + for _, plugin := range kfdef.Spec.Plugins { + p := kfconfig.Plugin{ + Name: plugin.Name, + Namespace: kfdef.Namespace, + Kind: pluginNameToKind(plugin.Name), + Spec: plugin.Spec, + } + config.Plugins = append(config.Plugins, p) + } + specCopiers := []func(*kfdeftypes.KfDef, *kfconfig.KfConfig) error{ + copyGcpPluginSpec, + } + for _, copier := range specCopiers { + if err := copier(kfdef, config); err != nil { + return nil, &kfapis.KfError{ + Code: int(kfapis.INVALID_ARGUMENT), + Message: fmt.Sprintf("error copying plugin specs: %v", err), + } + + } + } + + for _, secret := range kfdef.Spec.Secrets { + s := kfconfig.Secret{ + Name: secret.Name, + } + if secret.SecretSource == nil { + config.Secrets = append(config.Secrets, s) + continue + } + src := &kfconfig.SecretSource{} + if secret.SecretSource.LiteralSource != nil { + src.LiteralSource = &kfconfig.LiteralSource{ + Value: secret.SecretSource.LiteralSource.Value, + } + } else if secret.SecretSource.EnvSource != nil { + src.EnvSource = &kfconfig.EnvSource{ + Name: secret.SecretSource.EnvSource.Name, + } + } + s.SecretSource = src + config.Secrets = append(config.Secrets, s) + } + + for _, repo := range kfdef.Spec.Repos { + r := kfconfig.Repo{ + Name: repo.Name, + URI: repo.Uri, + } + config.Repos = append(config.Repos, r) + } + + for _, cond := range kfdef.Status.Conditions { + c := kfconfig.Condition{ + Type: kfconfig.ConditionType(cond.Type), + Status: cond.Status, + LastUpdateTime: cond.LastUpdateTime, + LastTransitionTime: cond.LastTransitionTime, + Reason: cond.Reason, + Message: cond.Message, + } + config.Status.Conditions = append(config.Status.Conditions, c) + } + for name, cache := range kfdef.Status.ReposCache { + c := kfconfig.Cache{ + Name: name, + LocalPath: cache.LocalPath, + } + config.Status.Caches = append(config.Status.Caches, c) + } + + return config, nil +} + +func (v V1alpha1) ToKfDefSerialized(config kfconfig.KfConfig) ([]byte, error) { + kfdef := &kfdeftypes.KfDef{} + kfdef.Name = config.Name + kfdef.Namespace = config.Namespace + kfdef.APIVersion = config.APIVersion + kfdef.Kind = "KfDef" + + kfdef.Spec.AppDir = config.AppDir + kfdef.Spec.UseBasicAuth = config.UseBasicAuth + // Should be deprecated, hardcode it just to be safe. + kfdef.Spec.EnableApplications = true + kfdef.Spec.UseIstio = true + kfdef.Spec.PackageManager = "kustomize" + + gcpSpec := &kfgcp.GcpPluginSpec{} + if err := config.GetPluginSpec(kfconfig.GCP_PLUGIN_KIND, gcpSpec); err == nil { + kfdef.Spec.Project = gcpSpec.Project + kfdef.Spec.Email = gcpSpec.Email + kfdef.Spec.IpName = gcpSpec.IpName + kfdef.Spec.Hostname = gcpSpec.Hostname + kfdef.Spec.Zone = gcpSpec.Zone + kfdef.Spec.SkipInitProject = gcpSpec.SkipInitProject + kfdef.Spec.DeleteStorage = gcpSpec.DeleteStorage + } + + for _, app := range config.Applications { + application := kfdeftypes.Application{ + Name: app.Name, + } + if app.KustomizeConfig != nil { + kconfig := &kfdeftypes.KustomizeConfig{ + Overlays: app.KustomizeConfig.Overlays, + } + if app.KustomizeConfig.RepoRef != nil { + kref := &kfdeftypes.RepoRef{ + Name: app.KustomizeConfig.RepoRef.Name, + Path: app.KustomizeConfig.RepoRef.Path, + } + kconfig.RepoRef = kref + } + for _, param := range app.KustomizeConfig.Parameters { + p := configsv3.NameValue{ + Name: param.Name, + Value: param.Value, + } + kconfig.Parameters = append(kconfig.Parameters, p) + } + application.KustomizeConfig = kconfig + } + kfdef.Spec.Applications = append(kfdef.Spec.Applications, application) + } + + platform := "" + for _, plugin := range config.Plugins { + p := kfdeftypes.Plugin{ + Name: plugin.Name, + Spec: plugin.Spec, + } + kfdef.Spec.Plugins = append(kfdef.Spec.Plugins, p) + + if plugin.Name == kftypesv3.AWS { + platform = kftypesv3.AWS + } else if plugin.Name == kftypesv3.GCP { + platform = kftypesv3.GCP + } + } + if platform == "" { + return []byte(""), &kfapis.KfError{ + Code: int(kfapis.INVALID_ARGUMENT), + Message: "Not able to find platform in plugins", + } + } + kfdef.Spec.Platform = platform + + for _, secret := range config.Secrets { + s := kfdeftypes.Secret{ + Name: secret.Name, + } + if secret.SecretSource != nil { + s.SecretSource = &kfdeftypes.SecretSource{} + if secret.SecretSource.LiteralSource != nil { + s.SecretSource.LiteralSource = &kfdeftypes.LiteralSource{ + Value: secret.SecretSource.LiteralSource.Value, + } + } + if secret.SecretSource.EnvSource != nil { + s.SecretSource.EnvSource = &kfdeftypes.EnvSource{ + Name: secret.SecretSource.EnvSource.Name, + } + } + } + kfdef.Spec.Secrets = append(kfdef.Spec.Secrets, s) + } + + for _, repo := range config.Repos { + r := kfdeftypes.Repo{ + Name: repo.Name, + Uri: repo.URI, + } + kfdef.Spec.Repos = append(kfdef.Spec.Repos, r) + } + + for _, cond := range config.Status.Conditions { + c := kfdeftypes.KfDefCondition{ + Type: kfdeftypes.KfDefConditionType(cond.Type), + Status: cond.Status, + LastUpdateTime: cond.LastUpdateTime, + LastTransitionTime: cond.LastTransitionTime, + Reason: cond.Reason, + Message: cond.Message, + } + kfdef.Status.Conditions = append(kfdef.Status.Conditions, c) + } + + for _, cache := range config.Status.Caches { + kfdef.Status.ReposCache[cache.Name] = kfdeftypes.RepoCache{ + LocalPath: cache.LocalPath, + } + } + + kfdefBytes, err := yaml.Marshal(kfdef) + if err == nil { + return kfdefBytes, nil + } else { + return nil, &kfapis.KfError{ + Code: int(kfapis.INTERNAL_ERROR), + Message: fmt.Sprintf("error when marshaling to KfDef: %v", err), + } + } +} diff --git a/bootstrap/pkg/apis/apps/configconverters/v1alpha1_test.go b/bootstrap/pkg/apis/apps/configconverters/v1alpha1_test.go new file mode 100644 index 00000000000..01e0544af45 --- /dev/null +++ b/bootstrap/pkg/apis/apps/configconverters/v1alpha1_test.go @@ -0,0 +1,132 @@ +package configconverters + +import ( + "github.com/ghodss/yaml" + kftypes "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis/apps" + kfconfig "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis/apps/kfconfig" + kfdeftypes "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis/apps/kfdef/v1alpha1" + kfgcp "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/kfapp/gcp" + kfutils "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/utils" + "io/ioutil" + "os" + "path" + "reflect" + "testing" +) + +func TestV1alpha1_ConvertToKfConfigs(t *testing.T) { + type testCase struct { + Input string + Expected string + } + + cases := []testCase{ + testCase{ + Input: "v1alpha1.yaml", + Expected: "kfconfig_v1alpha1.yaml", + }, + } + + for _, c := range cases { + wd, _ := os.Getwd() + fPath := path.Join(wd, "testdata", c.Input) + + buf, bufErr := ioutil.ReadFile(fPath) + if bufErr != nil { + t.Fatalf("Error reading file %v; error %v", fPath, bufErr) + } + + v1alpha1 := V1alpha1{} + config, err := v1alpha1.ToKfConfig("", buf) + if err != nil { + t.Fatalf("Error converting to KfConfig: %v", err) + } + + ePath := path.Join(wd, "testdata", c.Expected) + eBuf, err := ioutil.ReadFile(ePath) + if err != nil { + t.Fatalf("Error when reading KfConfig: %v", err) + } + expectedConfig := &kfconfig.KfConfig{} + err = yaml.Unmarshal(eBuf, expectedConfig) + if err != nil { + t.Fatalf("Error when unmarshaling KfConfig: %v", err) + } + + if !reflect.DeepEqual(config, expectedConfig) { + pGot := kfutils.PrettyPrint(config) + pWant := kfutils.PrettyPrint(expectedConfig) + t.Errorf("Loaded KfConfig doesn't match;\nexpected\n%v\ngot\n%v\n", pWant, pGot) + } + } +} + +func TestV1alpha1_ConvertToKfDef(t *testing.T) { + type testCase struct { + Input string + Expected string + } + + cases := []testCase{ + testCase{ + Input: "kfconfig_v1alpha1.yaml", + Expected: "v1alpha1.yaml", + }, + } + + for _, c := range cases { + wd, _ := os.Getwd() + fPath := path.Join(wd, "testdata", c.Input) + + buf, bufErr := ioutil.ReadFile(fPath) + if bufErr != nil { + t.Fatalf("Error reading file %v; error %v", fPath, bufErr) + } + config := &kfconfig.KfConfig{} + err := yaml.Unmarshal(buf, config) + if err != nil { + t.Fatalf("Error when unmarshaling KfConfig: %v", err) + } + + v1alpha1 := V1alpha1{} + kfdefBytes, err := v1alpha1.ToKfDefSerialized(*config) + if err != nil { + t.Fatalf("Error converting to KfDef: %v", err) + } + got := &kfdeftypes.KfDef{} + err = yaml.Unmarshal(kfdefBytes, got) + if err != nil { + t.Fatalf("Error when unmarshaling to KfDef: %v", err) + } + gcpSpec := &kfgcp.GcpPluginSpec{} + err = got.GetPluginSpec(kftypes.GCP, gcpSpec) + if err != nil { + t.Fatalf("Error when getting spec: %v", err) + } + newSpec := &kfgcp.GcpPluginSpec{} + newSpec.CreatePipelinePersistentStorage = gcpSpec.CreatePipelinePersistentStorage + newSpec.EnableWorkloadIdentity = gcpSpec.EnableWorkloadIdentity + newSpec.DeploymentManagerConfig = gcpSpec.DeploymentManagerConfig + err = got.SetPluginSpec(kftypes.GCP, newSpec) + if err != nil { + t.Fatalf("Error when writing back GcpPluginSpec: %v", err) + } + + ePath := path.Join(wd, "testdata", c.Expected) + eBuf, err := ioutil.ReadFile(ePath) + if err != nil { + t.Fatalf("Error when reading KfDef: %v", err) + } + want := &kfdeftypes.KfDef{} + err = yaml.Unmarshal(eBuf, want) + if err != nil { + t.Fatalf("Error when unmarshaling to KfDef: %v", err) + } + + if !reflect.DeepEqual(got, want) { + pGot := kfutils.PrettyPrint(got) + pWant := kfutils.PrettyPrint(want) + t.Errorf("Loaded KfConfig doesn't match;\nexpected\n%v\ngot\n%v\n", pWant, pGot) + } + } +} diff --git a/bootstrap/pkg/apis/apps/configconverters/v1beta1.go b/bootstrap/pkg/apis/apps/configconverters/v1beta1.go new file mode 100644 index 00000000000..33eca00f977 --- /dev/null +++ b/bootstrap/pkg/apis/apps/configconverters/v1beta1.go @@ -0,0 +1,260 @@ +package configconverters + +import ( + "fmt" + "github.com/ghodss/yaml" + kfapis "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis" + kfconfig "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis/apps/kfconfig" + kfdeftypes "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis/apps/kfdef/v1beta1" +) + +// Empty struct - used to implement Converter interface. +type V1beta1 struct { +} + +func (v V1beta1) ToKfConfig(appdir string, kfdefBytes []byte) (*kfconfig.KfConfig, error) { + kfdef := &kfdeftypes.KfDef{} + if err := yaml.Unmarshal(kfdefBytes, kfdef); err != nil { + return nil, &kfapis.KfError{ + Code: int(kfapis.INTERNAL_ERROR), + Message: fmt.Sprintf("could not unmarshal config file onto KfDef struct: %v", err), + } + } + + // Set UseBasicAuth later. + config := &kfconfig.KfConfig{ + AppDir: appdir, + UseBasicAuth: false, + } + config.Name = kfdef.Name + config.Namespace = kfdef.Namespace + config.APIVersion = kfdef.APIVersion + config.Kind = "KfConfig" + for _, app := range kfdef.Spec.Applications { + application := kfconfig.Application{ + Name: app.Name, + } + if app.KustomizeConfig != nil { + kconfig := &kfconfig.KustomizeConfig{ + Overlays: app.KustomizeConfig.Overlays, + } + if app.KustomizeConfig.RepoRef != nil { + kref := &kfconfig.RepoRef{ + Name: app.KustomizeConfig.RepoRef.Name, + Path: app.KustomizeConfig.RepoRef.Path, + } + kconfig.RepoRef = kref + + // Use application to infer whether UseBasicAuth is true. + if kref.Path == "common/basic-auth" { + config.UseBasicAuth = true + } + } + for _, param := range app.KustomizeConfig.Parameters { + p := kfconfig.NameValue{ + Name: param.Name, + Value: param.Value, + } + kconfig.Parameters = append(kconfig.Parameters, p) + } + application.KustomizeConfig = kconfig + } + config.Applications = append(config.Applications, application) + } + + for _, plugin := range kfdef.Spec.Plugins { + p := kfconfig.Plugin{ + Name: plugin.Name, + Namespace: kfdef.Namespace, + Kind: kfconfig.PluginKindType(plugin.Kind), + Spec: plugin.Spec, + } + config.Plugins = append(config.Plugins, p) + + if plugin.Kind == string(kfconfig.GCP_PLUGIN_KIND) { + specBytes, err := yaml.Marshal(plugin.Spec) + if err != nil { + return nil, &kfapis.KfError{ + Code: int(kfapis.INTERNAL_ERROR), + Message: fmt.Sprintf("could not marshal GCP plugin spec: %v", err), + } + } + var s map[string]interface{} + err = yaml.Unmarshal(specBytes, &s) + if err != nil { + return nil, &kfapis.KfError{ + Code: int(kfapis.INTERNAL_ERROR), + Message: fmt.Sprintf("could not unmarshal GCP plugin spec: %v", err), + } + } + if p, ok := s["project"]; ok { + config.Project = p.(string) + } + if e, ok := s["email"]; ok { + config.Email = e.(string) + } + if i, ok := s["ipName"]; ok { + config.IpName = i.(string) + } + if h, ok := s["hostname"]; ok { + config.Hostname = h.(string) + } + if z, ok := s["zone"]; ok { + config.Zone = z.(string) + } + } + } + + for _, secret := range kfdef.Spec.Secrets { + s := kfconfig.Secret{ + Name: secret.Name, + } + if secret.SecretSource == nil { + config.Secrets = append(config.Secrets, s) + continue + } + src := &kfconfig.SecretSource{} + if secret.SecretSource.LiteralSource != nil { + src.LiteralSource = &kfconfig.LiteralSource{ + Value: secret.SecretSource.LiteralSource.Value, + } + } else if secret.SecretSource.EnvSource != nil { + src.EnvSource = &kfconfig.EnvSource{ + Name: secret.SecretSource.EnvSource.Name, + } + } + s.SecretSource = src + config.Secrets = append(config.Secrets, s) + } + + for _, repo := range kfdef.Spec.Repos { + r := kfconfig.Repo{ + Name: repo.Name, + URI: repo.URI, + } + config.Repos = append(config.Repos, r) + } + + for _, cond := range kfdef.Status.Conditions { + c := kfconfig.Condition{ + Type: kfconfig.ConditionType(cond.Type), + Status: cond.Status, + LastUpdateTime: cond.LastUpdateTime, + LastTransitionTime: cond.LastTransitionTime, + Reason: cond.Reason, + Message: cond.Message, + } + config.Status.Conditions = append(config.Status.Conditions, c) + } + for _, cache := range kfdef.Status.ReposCache { + c := kfconfig.Cache{ + Name: cache.Name, + LocalPath: cache.LocalPath, + } + config.Status.Caches = append(config.Status.Caches, c) + } + + return config, nil + +} + +func (v V1beta1) ToKfDefSerialized(config kfconfig.KfConfig) ([]byte, error) { + kfdef := &kfdeftypes.KfDef{} + kfdef.Name = config.Name + kfdef.Namespace = config.Namespace + kfdef.APIVersion = config.APIVersion + kfdef.Kind = "KfDef" + + for _, app := range config.Applications { + application := kfdeftypes.Application{ + Name: app.Name, + } + if app.KustomizeConfig != nil { + kconfig := &kfdeftypes.KustomizeConfig{ + Overlays: app.KustomizeConfig.Overlays, + } + if app.KustomizeConfig.RepoRef != nil { + kref := &kfdeftypes.RepoRef{ + Name: app.KustomizeConfig.RepoRef.Name, + Path: app.KustomizeConfig.RepoRef.Path, + } + kconfig.RepoRef = kref + } + for _, param := range app.KustomizeConfig.Parameters { + p := kfdeftypes.NameValue{ + Name: param.Name, + Value: param.Value, + } + kconfig.Parameters = append(kconfig.Parameters, p) + } + application.KustomizeConfig = kconfig + } + kfdef.Spec.Applications = append(kfdef.Spec.Applications, application) + } + + for _, plugin := range config.Plugins { + p := kfdeftypes.Plugin{ + Spec: plugin.Spec, + } + p.Name = plugin.Name + kfdef.Spec.Plugins = append(kfdef.Spec.Plugins, p) + } + + for _, secret := range config.Secrets { + s := kfdeftypes.Secret{ + Name: secret.Name, + } + if secret.SecretSource != nil { + s.SecretSource = &kfdeftypes.SecretSource{} + if secret.SecretSource.LiteralSource != nil { + s.SecretSource.LiteralSource = &kfdeftypes.LiteralSource{ + Value: secret.SecretSource.LiteralSource.Value, + } + } + if secret.SecretSource.EnvSource != nil { + s.SecretSource.EnvSource = &kfdeftypes.EnvSource{ + Name: secret.SecretSource.EnvSource.Name, + } + } + } + kfdef.Spec.Secrets = append(kfdef.Spec.Secrets, s) + } + + for _, repo := range config.Repos { + r := kfdeftypes.Repo{ + Name: repo.Name, + URI: repo.URI, + } + kfdef.Spec.Repos = append(kfdef.Spec.Repos, r) + } + + for _, cond := range config.Status.Conditions { + c := kfdeftypes.KfDefCondition{ + Type: kfdeftypes.KfDefConditionType(cond.Type), + Status: cond.Status, + LastUpdateTime: cond.LastUpdateTime, + LastTransitionTime: cond.LastTransitionTime, + Reason: cond.Reason, + Message: cond.Message, + } + kfdef.Status.Conditions = append(kfdef.Status.Conditions, c) + } + + for _, cache := range config.Status.Caches { + c := kfdeftypes.RepoCache{ + Name: cache.Name, + LocalPath: cache.LocalPath, + } + kfdef.Status.ReposCache = append(kfdef.Status.ReposCache, c) + } + + kfdefBytes, err := yaml.Marshal(kfdef) + if err == nil { + return kfdefBytes, nil + } else { + return nil, &kfapis.KfError{ + Code: int(kfapis.INTERNAL_ERROR), + Message: fmt.Sprintf("error when marshaling to KfDef: %v", err), + } + } +} diff --git a/bootstrap/pkg/apis/apps/configconverters/v1beta1_test.go b/bootstrap/pkg/apis/apps/configconverters/v1beta1_test.go new file mode 100644 index 00000000000..359353adec8 --- /dev/null +++ b/bootstrap/pkg/apis/apps/configconverters/v1beta1_test.go @@ -0,0 +1,60 @@ +package configconverters + +import ( + "github.com/ghodss/yaml" + kfconfig "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis/apps/kfconfig" + kfutils "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/utils" + "io/ioutil" + "os" + "path" + "reflect" + "testing" +) + +func TestV1beta1_expectedConfig(t *testing.T) { + type testCase struct { + Input string + Expected string + } + + cases := []testCase{ + testCase{ + Input: "v1beta1.yaml", + Expected: "kfconfig_v1beta1.yaml", + }, + } + + for _, c := range cases { + wd, _ := os.Getwd() + fPath := path.Join(wd, "testdata", c.Input) + + buf, bufErr := ioutil.ReadFile(fPath) + if bufErr != nil { + t.Fatalf("Error reading file %v; error %v", fPath, bufErr) + } + + v1beta1 := V1beta1{} + config, err := v1beta1.ToKfConfig("", buf) + if err != nil { + t.Fatalf("Error converting to KfConfig: %v", err) + } + + ePath := path.Join(wd, "testdata", c.Expected) + eBuf, err := ioutil.ReadFile(ePath) + if err != nil { + t.Fatalf("Error when reading KfConfig: %v", err) + } + expectedConfig := &kfconfig.KfConfig{} + err = yaml.Unmarshal(eBuf, expectedConfig) + if err != nil { + t.Fatalf("Error when unmarshaling KfConfig: %v", err) + } + + if !reflect.DeepEqual(config, expectedConfig) { + pGot := kfutils.PrettyPrint(config) + pWant := kfutils.PrettyPrint(expectedConfig) + t.Errorf("Loaded KfConfig doesn't match;\nexpected\n%v\ngot\n%v\n", pWant, pGot) + } + } + +} diff --git a/bootstrap/pkg/apis/apps/kfconfig/types.go b/bootstrap/pkg/apis/apps/kfconfig/types.go new file mode 100644 index 00000000000..7a67c33b9b4 --- /dev/null +++ b/bootstrap/pkg/apis/apps/kfconfig/types.go @@ -0,0 +1,362 @@ +package kfconfig + +import ( + "fmt" + "github.com/ghodss/yaml" + kfapis "github.com/kubeflow/kfctl/v3/pkg/apis" + log "github.com/sirupsen/logrus" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "strings" +) + +// Internal data structure to hold app related info. +type KfConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + // Shared fields among all components. should limit this list. + // TODO(gabrielwen): Deprecate AppDir and move it to cache in Status. + AppDir string `json:"appDir,omitempty"` + // TODO(gabrielwen): Can we infer this from Applications? + UseBasicAuth bool `json:"useBasicAuth,omitempty"` + + // TODO(gabrielwen): Deprecate these fields as they only makes sense to GCP. + Project string `json:"project,omitempty"` + Email string `json:"email,omitempty"` + IpName string `json:"ipName,omitempty"` + Hostname string `json:"hostname,omitempty"` + Zone string `json:"zone,omitempty"` + + Applications []Application `json:"applications,omitempty"` + Plugins []Plugin `json:"plugins,omitempty"` + Secrets []Secret `json:"secrets,omitempty"` + Repos []Repo `json:"repos,omitempty"` + Status Status `json:"status,omitempty"` +} + +// Application defines an application to install +type Application struct { + Name string `json:"name,omitempty"` + KustomizeConfig *KustomizeConfig `json:"kustomizeConfig,omitempty"` +} + +type KustomizeConfig struct { + RepoRef *RepoRef `json:"repoRef,omitempty"` + Overlays []string `json:"overlays,omitempty"` + Parameters []NameValue `json:"parameters,omitempty"` +} + +type RepoRef struct { + Name string `json:"name,omitempty"` + Path string `json:"path,omitempty"` +} + +type NameValue struct { + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` +} + +type Plugin struct { + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` + Kind PluginKindType `json:"kind,omitempty"` + Spec *runtime.RawExtension `json:"spec,omitempty"` +} + +// Secret provides information about secrets needed to configure Kubeflow. +// Secrets can be provided via references. +type Secret struct { + Name string `json:"name,omitempty"` + SecretSource *SecretSource `json:"secretSource,omitempty"` +} + +type SecretSource struct { + LiteralSource *LiteralSource `json:"literalSource,omitempty"` + EnvSource *EnvSource `json:"envSource,omitempty"` +} + +type LiteralSource struct { + Value string `json:"value,omitempty"` +} + +type EnvSource struct { + Name string `json:"name,omitempty"` +} + +// SecretRef is a reference to a secret +type SecretRef struct { + // Name of the secret + Name string `json:"name,omitempty"` +} + +// Repo provides information about a repository providing config (e.g. kustomize packages, +// Deployment manager configs, etc...) +type Repo struct { + // Name is a name to identify the repository. + Name string `json:"name,omitempty"` + // URI where repository can be obtained. + // Can use any URI understood by go-getter: + // https://github.com/hashicorp/go-getter/blob/master/README.md#installation-and-usage + URI string `json:"uri,omitempty"` +} + +type Status struct { + Conditions []Condition `json:"conditions,omitempty"` + Caches []Cache `json:"caches,omitempty"` +} + +type Condition struct { + // Type of deployment condition. + Type ConditionType `json:"type,omitempty"` + // Status of the condition, one of True, False, Unknown. + Status v1.ConditionStatus `json:"status,omitempty"` + // The last time this condition was updated. + LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty"` + // Last time the condition transitioned from one status to another. + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` + // The reason for the condition's last transition. + Reason string `json:"reason,omitempty"` + // A human readable message indicating details about the transition. + Message string `json:"message,omitempty"` +} + +type PluginKindType string + +const ( + // Used for populating plugin missing errors and identifying those + // errors. + pluginNotFoundErrPrefix = "Missing plugin" + + // Used for populating plugin missing errors and identifying those + // errors. + conditionNotFoundErrPrefix = "Missing condition" +) + +// Plugin kind used starting from v1beta1 +const ( + AWS_PLUGIN_KIND PluginKindType = "KfAwsPlugin" + GCP_PLUGIN_KIND PluginKindType = "KfGcpPlugin" + MINIKUBE_PLUGIN_KIND PluginKindType = "KfMinikubePlugin" + EXISTING_ARRIKTO_PLUGIN_KIND PluginKindType = "KfExistingArriktoPlugin" +) + +type ConditionType string + +const ( + // KfAvailable means Kubeflow is serving. + Available ConditionType = "Available" + + // KfDegraded means functionality of Kubeflow is limited. + Degraded ConditionType = "Degraded" +) + +// Define plugin related conditions to be the format: +// - conditions for successful plugins: ${PluginKind}Succeeded +// - conditions for failed plugins: ${PluginKind}Failed +func GetPluginSucceededCondition(pluginKind PluginKindType) ConditionType { + return ConditionType(fmt.Sprintf("%vSucceeded", pluginKind)) +} +func GetPluginFailedCondition(pluginKind PluginKindType) ConditionType { + return ConditionType(fmt.Sprintf("%vFailed", pluginKind)) +} + +type Cache struct { + Name string `json:"name,omitempty"` + LocalPath string `json:"localPath,omitempty"` +} + +func (c *KfConfig) GetPluginSpec(pluginKind PluginKindType, s interface{}) error { + for _, p := range c.Plugins { + if p.Kind != pluginKind { + continue + } + + // To deserialize it to a specific type we need to first serialize it to bytes + // and then unserialize it. + specBytes, err := yaml.Marshal(p.Spec) + if err != nil { + msg := fmt.Sprintf("Could not marshal plugin %v args; error %v", pluginKind, err) + log.Errorf(msg) + return &kfapis.KfError{ + Code: int(kfapis.INVALID_ARGUMENT), + Message: msg, + } + } + err = yaml.Unmarshal(specBytes, s) + if err != nil { + msg := fmt.Sprintf("Could not unmarshal plugin %v to the provided type; error %v", pluginKind, err) + log.Errorf(msg) + return &kfapis.KfError{ + Code: int(kfapis.INTERNAL_ERROR), + Message: msg, + } + } + return nil + } + return &kfapis.KfError{ + Code: int(kfapis.NOT_FOUND), + Message: fmt.Sprintf("%v %v", pluginNotFoundErrPrefix, pluginKind), + } +} + +// SetPluginSpec sets the requested parameter. The plugin is added if it doesn't already exist. +func (c *KfConfig) SetPluginSpec(pluginKind PluginKindType, spec interface{}) error { + // Convert spec to RawExtension + r := &runtime.RawExtension{} + + // To deserialize it to a specific type we need to first serialize it to bytes + // and then unserialize it. + specBytes, err := yaml.Marshal(spec) + + if err != nil { + msg := fmt.Sprintf("Could not marshal spec; error %v", err) + log.Errorf(msg) + return &kfapis.KfError{ + Code: int(kfapis.INVALID_ARGUMENT), + Message: msg, + } + } + + err = yaml.Unmarshal(specBytes, r) + + if err != nil { + msg := fmt.Sprintf("Could not unmarshal plugin to RawExtension; error %v", err) + log.Errorf(msg) + return &kfapis.KfError{ + Code: int(kfapis.INTERNAL_ERROR), + Message: msg, + } + } + + index := -1 + + for i, p := range c.Plugins { + if p.Kind == pluginKind { + index = i + break + } + } + + if index == -1 { + // Plugin in doesn't exist so add it + log.Infof("Adding plugin %v", pluginKind) + + p := Plugin{} + p.Name = string(pluginKind) + p.Kind = pluginKind + c.Plugins = append(c.Plugins, p) + + index = len(c.Plugins) - 1 + } + + c.Plugins[index].Spec = r + return nil +} + +// Sets condition and status to KfConfig. +func (c *KfConfig) SetCondition(condType ConditionType, + status v1.ConditionStatus, + reason string, + message string) { + now := metav1.Now() + cond := Condition{ + Type: condType, + Status: status, + LastUpdateTime: now, + LastTransitionTime: now, + Reason: reason, + Message: message, + } + + for i := range c.Status.Conditions { + if c.Status.Conditions[i].Type != condType { + continue + } + if c.Status.Conditions[i].Status == status { + cond.LastTransitionTime = c.Status.Conditions[i].LastTransitionTime + } + c.Status.Conditions[i] = cond + return + } + c.Status.Conditions = append(c.Status.Conditions, cond) +} + +// Gets condition from KfConfig. +func (c *KfConfig) GetCondition(condType ConditionType) (*Condition, error) { + for i := range c.Status.Conditions { + if c.Status.Conditions[i].Type == condType { + return &c.Status.Conditions[i], nil + } + } + return nil, &kfapis.KfError{ + Code: int(kfapis.NOT_FOUND), + Message: fmt.Sprintf("%v %v", conditionNotFoundErrPrefix, condType), + } +} + +func (c *KfConfig) IsPluginFinished(pluginKind PluginKindType) bool { + condType := GetPluginSucceededCondition(pluginKind) + cond, err := c.GetCondition(condType) + if err != nil { + if IsConditionNotFound(err) { + return false + } + log.Warnf("error when getting condition info: %v", err) + return false + } + return cond.Status == v1.ConditionTrue +} + +func (c *KfConfig) SetPluginFinished(pluginKind PluginKindType, msg string) { + succeededCond := GetPluginSucceededCondition(pluginKind) + failedCond := GetPluginFailedCondition(pluginKind) + if _, err := c.GetCondition(failedCond); err == nil { + c.SetCondition(failedCond, v1.ConditionFalse, "", + "Reset to false as the plugin is set to be finished.") + } + + c.SetCondition(succeededCond, v1.ConditionTrue, "", msg) +} + +func (c *KfConfig) IsPluginFailed(pluginKind PluginKindType) bool { + condType := GetPluginFailedCondition(pluginKind) + cond, err := c.GetCondition(condType) + if err != nil { + if IsConditionNotFound(err) { + return false + } + log.Warnf("error when getting condition info: %v", err) + return false + } + return cond.Status == v1.ConditionTrue +} + +func (c *KfConfig) SetPluginFailed(pluginKind PluginKindType, msg string) { + succeededCond := GetPluginSucceededCondition(pluginKind) + failedCond := GetPluginFailedCondition(pluginKind) + if _, err := c.GetCondition(succeededCond); err == nil { + c.SetCondition(succeededCond, v1.ConditionFalse, + "", "Reset to false as the plugin is set to be failed.") + } + + c.SetCondition(failedCond, v1.ConditionTrue, "", msg) +} + +func IsPluginNotFound(e error) bool { + if e == nil { + return false + } + err, ok := e.(*kfapis.KfError) + return ok && err.Code == int(kfapis.NOT_FOUND) && strings.HasPrefix(err.Message, pluginNotFoundErrPrefix) +} + +func IsConditionNotFound(e error) bool { + if e == nil { + return false + } + err, ok := e.(*kfapis.KfError) + return ok && err.Code == int(kfapis.NOT_FOUND) && + strings.HasPrefix(err.Message, conditionNotFoundErrPrefix) +} diff --git a/bootstrap/pkg/apis/apps/kfdef/expected.txt b/bootstrap/pkg/apis/apps/kfdef/expected.txt deleted file mode 100644 index bfead20ba4c..00000000000 --- a/bootstrap/pkg/apis/apps/kfdef/expected.txt +++ /dev/null @@ -1,92 +0,0 @@ -{ - "kind": "KfDef", - "apiVersion": "kfdef.apps.kubeflow.org/v1beta1", - "metadata": { - "creationTimestamp": null - }, - "spec": { - "applications": [ - { - "name": "istio-crds", - "kustomizeConfig": { - "repoRef": { - "name": "manifests", - "path": "istio/istio-crds" - }, - "parameters": [ - { - "name": "namespace", - "value": "istio-system" - } - ] - } - }, - { - "name": "istio-install", - "kustomizeConfig": { - "repoRef": { - "name": "manifests", - "path": "istio/istio-install" - }, - "parameters": [ - { - "name": "namespace", - "value": "istio-system" - } - ] - } - }, - { - "name": "istio", - "kustomizeConfig": { - "repoRef": { - "name": "manifests", - "path": "istio/istio" - }, - "parameters": [ - { - "name": "clusterRbacConfig", - "value": "ON" - } - ] - } - } - ], - "plugins": [ - { - "kind": "KfGcpPlugin", - "metadata": { - "name": "gcp", - "creationTimestamp": null - }, - "spec": { - "auth": { - "iap": { - "oAuthClientId": "foo-user", - "oAuthClientSecret": { - "name": "CLIENT_SECRET" - } - } - }, - "createPipelinePersistentStorage": true, - "deleteStorage": true, - "email": "foo@gmail.com", - "enableApplications": true, - "hostname": "foo.endpoints.foo-project.cloud.goog", - "ipName": "foo-ip", - "project": "foo-project", - "skipInitProject": true, - "useBasicAuth": false, - "zone": "us-east1-b" - } - } - ], - "repos": [ - { - "name": "manifests", - "uri": "https://github.com/kubeflow/manifests/archive/master.tar.gz" - } - ] - }, - "status": {} -} \ No newline at end of file diff --git a/bootstrap/pkg/apis/apps/kfdef/kfloader.go b/bootstrap/pkg/apis/apps/kfdef/kfloader.go deleted file mode 100644 index 8719ec48a23..00000000000 --- a/bootstrap/pkg/apis/apps/kfdef/kfloader.go +++ /dev/null @@ -1,289 +0,0 @@ -package kfdef - -import ( - "fmt" - "github.com/ghodss/yaml" - gogetter "github.com/hashicorp/go-getter" - kfapis "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis" - kftypesv3 "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis/apps" - kfdefv1alpha1 "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis/apps/kfdef/v1alpha1" - kfdefv1beta1 "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis/apps/kfdef/v1beta1" - kfgcp "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/kfapp/gcp" - log "github.com/sirupsen/logrus" - "io/ioutil" - "k8s.io/apimachinery/pkg/runtime" - netUrl "net/url" - "path" - "strings" -) - -const ( - KfConfigFile = "app.yaml" - Api = "kfdef.apps.kubeflow.org" -) - -func isValidUrl(toTest string) bool { - _, err := netUrl.ParseRequestURI(toTest) - if err != nil { - return false - } else { - return true - } -} - -// Simple mapping from plugin name to plugin kind in v1beta1. Ideally we should -// use plugin kind to find handler functions. Only used for backward compatibility. -func alphaPluginNameToBetaKind(pluginName string) kfdefv1beta1.PluginKindType { - mapping := map[string]kfdefv1beta1.PluginKindType{ - kftypesv3.AWS: kfdefv1beta1.AWS_PLUGIN_KIND, - kftypesv3.GCP: kfdefv1beta1.GCP_PLUGIN_KIND, - kftypesv3.MINIKUBE: kfdefv1beta1.MINIKUBE_PLUGIN_KIND, - kftypesv3.EXISTING_ARRIKTO: kfdefv1beta1.EXISTING_ARRIKTO_PLUGIN_KIND, - } - - kind, ok := mapping[pluginName] - if ok { - return kind - } else { - return "KfUnknownPlugin" - } -} - -// Copy application configs. -func copyApplications(from *kfdefv1alpha1.KfDef, to *kfdefv1beta1.KfDef) { - for _, application := range from.Spec.Applications { - app := kfdefv1beta1.Application{ - Name: application.Name, - KustomizeConfig: &kfdefv1beta1.KustomizeConfig{ - RepoRef: &kfdefv1beta1.RepoRef{ - Name: application.KustomizeConfig.RepoRef.Name, - Path: application.KustomizeConfig.RepoRef.Path, - }, - Overlays: application.KustomizeConfig.Overlays, - }, - } - for _, param := range application.KustomizeConfig.Parameters { - app.KustomizeConfig.Parameters = append(app.KustomizeConfig.Parameters, - kfdefv1beta1.NameValue{ - Name: param.Name, - Value: param.Value, - }) - } - to.Spec.Applications = append(to.Spec.Applications, app) - } -} - -// Copy repos configs. -func copyRepos(from *kfdefv1alpha1.KfDef, to *kfdefv1beta1.KfDef) { - for _, repo := range from.Spec.Repos { - betaRepo := kfdefv1beta1.Repo{ - Name: repo.Name, - URI: repo.Uri, - } - to.Spec.Repos = append(to.Spec.Repos, betaRepo) - } -} - -// Copy secrets configs. -func copySecrets(from *kfdefv1alpha1.KfDef, to *kfdefv1beta1.KfDef) { - for _, secret := range from.Spec.Secrets { - betaSecret := kfdefv1beta1.Secret{ - Name: secret.Name, - } - if secret.SecretSource != nil { - betaSecret.SecretSource = &kfdefv1beta1.SecretSource{} - if secret.SecretSource.LiteralSource != nil { - betaSecret.SecretSource.LiteralSource = &kfdefv1beta1.LiteralSource{ - Value: secret.SecretSource.LiteralSource.Value, - } - } - if secret.SecretSource.EnvSource != nil { - betaSecret.SecretSource.EnvSource = &kfdefv1beta1.EnvSource{ - Name: secret.SecretSource.EnvSource.Name, - } - } - } - to.Spec.Secrets = append(to.Spec.Secrets, betaSecret) - } -} - -// Copy GCP plugin spec. Will skip if platform is not GCP. -func copyGcpPluginSpec(from *kfdefv1alpha1.KfDef, to *kfdefv1beta1.KfDef) error { - if from.Spec.Platform != kftypesv3.GCP { - return nil - } - - spec := kfgcp.GcpPluginSpec{} - if err := to.GetPluginSpec(kfdefv1beta1.GCP_PLUGIN_KIND, &spec); err != nil && !kfdefv1beta1.IsPluginNotFound(err) { - return err - } - spec.Project = from.Spec.Project - spec.Email = from.Spec.Email - spec.IpName = from.Spec.IpName - spec.Hostname = from.Spec.Hostname - spec.Zone = from.Spec.Zone - spec.UseBasicAuth = from.Spec.UseBasicAuth - spec.SkipInitProject = from.Spec.SkipInitProject - spec.DeleteStorage = from.Spec.DeleteStorage - return to.SetPluginSpec(kfdefv1beta1.GCP_PLUGIN_KIND, spec) -} - -// Copy plugins configs. -func copyPlugins(from *kfdefv1alpha1.KfDef, to *kfdefv1beta1.KfDef) { - for _, plugin := range from.Spec.Plugins { - betaPlugin := kfdefv1beta1.Plugin{} - betaPlugin.Name = plugin.Name - betaPlugin.Kind = string(alphaPluginNameToBetaKind(plugin.Name)) - if plugin.Spec != nil { - betaPlugin.Spec = &runtime.RawExtension{} - *betaPlugin.Spec = *plugin.Spec - } - to.Spec.Plugins = append(to.Spec.Plugins, betaPlugin) - } - - specCopiers := []func(*kfdefv1alpha1.KfDef, *kfdefv1beta1.KfDef) error{ - copyGcpPluginSpec, - } - for _, copier := range specCopiers { - if err := copier(from, to); err != nil { - log.Errorf("having error when copying plugin spec: %v", err) - } - } -} - -// Load v1alpha1 KfDef and returns v1beta1 KfDef. -func loadKfDefV1Alpha1(configs []byte) (*kfdefv1beta1.KfDef, error) { - alphaKfDef := &kfdefv1alpha1.KfDef{} - if err := yaml.Unmarshal(configs, alphaKfDef); err != nil { - return nil, &kfapis.KfError{ - Code: int(kfapis.INVALID_ARGUMENT), - Message: fmt.Sprintf("invalid config file format: %v", err), - } - } - - betaKfDef := &kfdefv1beta1.KfDef{ - TypeMeta: alphaKfDef.TypeMeta, - ObjectMeta: alphaKfDef.ObjectMeta, - } - betaKfDef.APIVersion = Api + "/v1beta1" - - // Converting functions wrapper. - converters := []func(*kfdefv1alpha1.KfDef, *kfdefv1beta1.KfDef){ - copyApplications, - copyRepos, - copySecrets, - copyPlugins, - } - for _, converter := range converters { - converter(alphaKfDef, betaKfDef) - } - - return betaKfDef, nil -} - -// Load v1beta1 KfDef. -func loadKfDefV1Beta1(configs []byte) (*kfdefv1beta1.KfDef, error) { - kfdef := &kfdefv1beta1.KfDef{} - if err := yaml.Unmarshal(configs, kfdef); err != nil { - return nil, &kfapis.KfError{ - Code: int(kfapis.INVALID_ARGUMENT), - Message: fmt.Sprintf("invalid config file format: %v", err), - } - } - return kfdef, nil -} - -// Loads KfDef file and returns KfDef in v1beta1. v1alpha1 will be converted to the new version. -func LoadKfDefFromURI(configFile string) (*kfdefv1beta1.KfDef, error) { - if configFile == "" { - return nil, fmt.Errorf("config file must be the URI of a KfDef spec") - } - - // TODO(jlewi): We should check if configFile doesn't specify a protocol or the protocol - // is file:// then we can just read it rather than fetching with go-getter. - appDir, err := ioutil.TempDir("", "") - if err != nil { - return nil, fmt.Errorf("Create a temporary directory to copy the file to.") - } - // Open config file - // - // TODO(jlewi): Should we use hashicorp go-getter.GetAny here? We use that to download - // the tarballs for the repos. Maybe we should use that here as well to be consistent. - appFile := path.Join(appDir, KfConfigFile) - - log.Infof("Downloading %v to %v", configFile, appFile) - configFileUri, err := netUrl.Parse(configFile) - if err != nil { - log.Errorf("could not parse configFile url") - } - if isValidUrl(configFile) { - errGet := gogetter.GetFile(appFile, configFile) - if errGet != nil { - return nil, &kfapis.KfError{ - Code: int(kfapis.INVALID_ARGUMENT), - Message: fmt.Sprintf("could not fetch specified config %s: %v", configFile, err), - } - } - } else { - g := new(gogetter.FileGetter) - g.Copy = true - errGet := g.GetFile(appFile, configFileUri) - if errGet != nil { - return nil, &kfapis.KfError{ - Code: int(kfapis.INVALID_ARGUMENT), - Message: fmt.Sprintf("could not fetch specified config %s: %v", configFile, err), - } - } - } - - // Read contents - configFileBytes, err := ioutil.ReadFile(appFile) - if err != nil { - return nil, &kfapis.KfError{ - Code: int(kfapis.INTERNAL_ERROR), - Message: fmt.Sprintf("could not read from config file %s: %v", configFile, err), - } - } - - // Check API version. - var obj map[string]interface{} - if err = yaml.Unmarshal(configFileBytes, &obj); err != nil { - return nil, &kfapis.KfError{ - Code: int(kfapis.INVALID_ARGUMENT), - Message: fmt.Sprintf("invalid config file format: %v", err), - } - } - apiVersion, ok := obj["apiVersion"] - if !ok { - return nil, &kfapis.KfError{ - Code: int(kfapis.INVALID_ARGUMENT), - Message: "invalid config: apiVersion is not found.", - } - } - apiVersionSeparated := strings.Split(apiVersion.(string), "/") - if len(apiVersionSeparated) < 2 || apiVersionSeparated[0] != Api { - return nil, &kfapis.KfError{ - Code: int(kfapis.INVALID_ARGUMENT), - Message: fmt.Sprintf("invalid config: apiVersion must be in the format of %v/, got %v", Api, apiVersion), - } - } - - loaders := map[string]func([]byte) (*kfdefv1beta1.KfDef, error){ - "v1alpha1": loadKfDefV1Alpha1, - "v1beta1": loadKfDefV1Beta1, - } - - loader, ok := loaders[apiVersionSeparated[1]] - if !ok { - versions := []string{} - for key := range loaders { - versions = append(versions, key) - } - return nil, &kfapis.KfError{ - Code: int(kfapis.INVALID_ARGUMENT), - Message: fmt.Sprintf("invalid config: version not supported; supported versions: %v, got %v", - strings.Join(versions, ", "), apiVersionSeparated[1]), - } - } - return loader(configFileBytes) -} diff --git a/bootstrap/pkg/apis/apps/kfdef/kfloader_test.go b/bootstrap/pkg/apis/apps/kfdef/kfloader_test.go deleted file mode 100644 index 9bba5bba297..00000000000 --- a/bootstrap/pkg/apis/apps/kfdef/kfloader_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package kfdef - -import ( - kfdefv1alpha1 "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis/apps/kfdef/v1alpha1" - kfdefv1beta1 "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis/apps/kfdef/v1beta1" - kfgcp "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/kfapp/gcp" - kfutils "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/utils" - "os" - "path" - "reflect" - "testing" -) - -func TestKfLoad_LoadKfDefFomURI(t *testing.T) { - type testCase struct { - filename string - expected *kfdefv1beta1.KfDef - } - - createPipelinePersistentStorage := true - expectedOutput := &kfdefv1beta1.KfDef{ - Spec: kfdefv1beta1.KfDefSpec{ - Repos: []kfdefv1beta1.Repo{ - kfdefv1beta1.Repo{ - Name: "manifests", - URI: "https://github.com/kubeflow/manifests/archive/master.tar.gz", - }, - }, - Applications: []kfdefv1beta1.Application{ - kfdefv1beta1.Application{ - Name: "istio-crds", - KustomizeConfig: &kfdefv1beta1.KustomizeConfig{ - RepoRef: &kfdefv1beta1.RepoRef{ - Name: "manifests", - Path: "istio/istio-crds", - }, - Parameters: []kfdefv1beta1.NameValue{ - kfdefv1beta1.NameValue{ - Name: "namespace", - Value: "istio-system", - }, - }, - }, - }, - kfdefv1beta1.Application{ - Name: "istio-install", - KustomizeConfig: &kfdefv1beta1.KustomizeConfig{ - RepoRef: &kfdefv1beta1.RepoRef{ - Name: "manifests", - Path: "istio/istio-install", - }, - Parameters: []kfdefv1beta1.NameValue{ - kfdefv1beta1.NameValue{ - Name: "namespace", - Value: "istio-system", - }, - }, - }, - }, - kfdefv1beta1.Application{ - Name: "istio", - KustomizeConfig: &kfdefv1beta1.KustomizeConfig{ - RepoRef: &kfdefv1beta1.RepoRef{ - Name: "manifests", - Path: "istio/istio", - }, - Parameters: []kfdefv1beta1.NameValue{ - kfdefv1beta1.NameValue{ - Name: "clusterRbacConfig", - Value: "ON", - }, - }, - }, - }, - }, - }, - } - expectedOutput.Kind = "KfDef" - expectedOutput.APIVersion = "kfdef.apps.kubeflow.org/v1beta1" - gcpPluginSpec := &kfgcp.GcpPluginSpec{ - Project: "foo-project", - Email: "foo@gmail.com", - IpName: "foo-ip", - Hostname: "foo.endpoints.foo-project.cloud.goog", - Zone: "us-east1-b", - UseBasicAuth: false, - SkipInitProject: true, - DeleteStorage: true, - CreatePipelinePersistentStorage: &createPipelinePersistentStorage, - Auth: &kfgcp.Auth{ - IAP: &kfgcp.IAP{ - OAuthClientId: "foo-user", - OAuthClientSecret: &kfdefv1alpha1.SecretRef{ - Name: "CLIENT_SECRET", - }, - }, - }, - } - if err := expectedOutput.SetPluginSpec("gcp", gcpPluginSpec); err != nil { - t.Fatalf("error when setting plugin spec: %v", err) - } - expectedOutput.Spec.Plugins[0].Kind = "KfGcpPlugin" - - testCases := []testCase{ - testCase{ - filename: "kfdef_v1alpha1.yaml", - expected: expectedOutput, - }, - testCase{ - filename: "kfdef_v1beta1.yaml", - expected: expectedOutput, - }, - } - - for _, c := range testCases { - wd, _ := os.Getwd() - p := path.Join(wd, "testdata", c.filename) - kfdef, err := LoadKfDefFromURI(p) - if err != nil { - t.Fatalf("error when loading kfdef: %v", err) - } - if !reflect.DeepEqual(kfdef, c.expected) { - t.Errorf("KfDef loaded doesn't match expected;\nloaded\n%v\nexpected\n%v", - kfutils.PrettyPrint(kfdef), - kfutils.PrettyPrint(*c.expected)) - } - } -} diff --git a/bootstrap/pkg/apis/apps/kfdef/read.txt b/bootstrap/pkg/apis/apps/kfdef/read.txt deleted file mode 100644 index bfead20ba4c..00000000000 --- a/bootstrap/pkg/apis/apps/kfdef/read.txt +++ /dev/null @@ -1,92 +0,0 @@ -{ - "kind": "KfDef", - "apiVersion": "kfdef.apps.kubeflow.org/v1beta1", - "metadata": { - "creationTimestamp": null - }, - "spec": { - "applications": [ - { - "name": "istio-crds", - "kustomizeConfig": { - "repoRef": { - "name": "manifests", - "path": "istio/istio-crds" - }, - "parameters": [ - { - "name": "namespace", - "value": "istio-system" - } - ] - } - }, - { - "name": "istio-install", - "kustomizeConfig": { - "repoRef": { - "name": "manifests", - "path": "istio/istio-install" - }, - "parameters": [ - { - "name": "namespace", - "value": "istio-system" - } - ] - } - }, - { - "name": "istio", - "kustomizeConfig": { - "repoRef": { - "name": "manifests", - "path": "istio/istio" - }, - "parameters": [ - { - "name": "clusterRbacConfig", - "value": "ON" - } - ] - } - } - ], - "plugins": [ - { - "kind": "KfGcpPlugin", - "metadata": { - "name": "gcp", - "creationTimestamp": null - }, - "spec": { - "auth": { - "iap": { - "oAuthClientId": "foo-user", - "oAuthClientSecret": { - "name": "CLIENT_SECRET" - } - } - }, - "createPipelinePersistentStorage": true, - "deleteStorage": true, - "email": "foo@gmail.com", - "enableApplications": true, - "hostname": "foo.endpoints.foo-project.cloud.goog", - "ipName": "foo-ip", - "project": "foo-project", - "skipInitProject": true, - "useBasicAuth": false, - "zone": "us-east1-b" - } - } - ], - "repos": [ - { - "name": "manifests", - "uri": "https://github.com/kubeflow/manifests/archive/master.tar.gz" - } - ] - }, - "status": {} -} \ No newline at end of file diff --git a/bootstrap/pkg/apis/apps/kfdef/testdata/kfdef_v1alpha1.yaml b/bootstrap/pkg/apis/apps/kfdef/testdata/kfdef_v1alpha1.yaml deleted file mode 100644 index 6ae3a94411d..00000000000 --- a/bootstrap/pkg/apis/apps/kfdef/testdata/kfdef_v1alpha1.yaml +++ /dev/null @@ -1,49 +0,0 @@ -apiVersion: kfdef.apps.kubeflow.org/v1alpha1 -kind: KfDef -spec: - platform: gcp - project: foo-project - email: foo@gmail.com - ipName: foo-ip - hostname: foo.endpoints.foo-project.cloud.goog - zone: us-east1-b - useBasicAuth: false - skipInitProject: true - deleteStorage: true - repos: - - name: manifests - uri: https://github.com/kubeflow/manifests/archive/master.tar.gz - applications: - - kustomizeConfig: - parameters: - - name: namespace - value: istio-system - repoRef: - name: manifests - path: istio/istio-crds - name: istio-crds - - kustomizeConfig: - parameters: - - name: namespace - value: istio-system - repoRef: - name: manifests - path: istio/istio-install - name: istio-install - - kustomizeConfig: - parameters: - - name: clusterRbacConfig - value: "ON" - repoRef: - name: manifests - path: istio/istio - name: istio - plugins: - - name: gcp - spec: - auth: - iap: - oAuthClientId: foo-user - oAuthClientSecret: - name: CLIENT_SECRET - createPipelinePersistentStorage: true diff --git a/bootstrap/pkg/apis/apps/kfdef/testdata/kfdef_v1beta1.yaml b/bootstrap/pkg/apis/apps/kfdef/testdata/kfdef_v1beta1.yaml deleted file mode 100644 index aa2f0ce95d0..00000000000 --- a/bootstrap/pkg/apis/apps/kfdef/testdata/kfdef_v1beta1.yaml +++ /dev/null @@ -1,50 +0,0 @@ -apiVersion: kfdef.apps.kubeflow.org/v1beta1 -kind: KfDef -spec: - repos: - - name: manifests - uri: https://github.com/kubeflow/manifests/archive/master.tar.gz - applications: - - kustomizeConfig: - parameters: - - name: namespace - value: istio-system - repoRef: - name: manifests - path: istio/istio-crds - name: istio-crds - - kustomizeConfig: - parameters: - - name: namespace - value: istio-system - repoRef: - name: manifests - path: istio/istio-install - name: istio-install - - kustomizeConfig: - parameters: - - name: clusterRbacConfig - value: "ON" - repoRef: - name: manifests - path: istio/istio - name: istio - plugins: - - kind: KfGcpPlugin - metadata: - name: gcp - spec: - project: foo-project - email: foo@gmail.com - ipName: foo-ip - hostname: foo.endpoints.foo-project.cloud.goog - zone: us-east1-b - useBasicAuth: false - skipInitProject: true - deleteStorage: true - auth: - iap: - oAuthClientId: foo-user - oAuthClientSecret: - name: CLIENT_SECRET - createPipelinePersistentStorage: true diff --git a/bootstrap/pkg/apis/apps/kfdef/v1beta1/application_types.go b/bootstrap/pkg/apis/apps/kfdef/v1beta1/application_types.go index 6a180cc126e..fcd6601cad1 100644 --- a/bootstrap/pkg/apis/apps/kfdef/v1beta1/application_types.go +++ b/bootstrap/pkg/apis/apps/kfdef/v1beta1/application_types.go @@ -15,26 +15,13 @@ package v1beta1 import ( - "fmt" - "github.com/ghodss/yaml" - kfapis "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/apis" - log "github.com/sirupsen/logrus" "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "strings" ) const ( KfConfigFile = "app.yaml" - - // Used for populating plugin missing errors and identifying those - // errors. - pluginNotFoundErrPrefix = "Missing plugin" - - // Used for populating plugin missing errors and identifying those - // errors. - conditionNotFoundErrPrefix = "Missing condition" ) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -87,16 +74,6 @@ type NameValue struct { Value string `json:"value,omitempty"` } -type PluginKindType string - -// Plugin kind used starting from v1beta1 -const ( - AWS_PLUGIN_KIND PluginKindType = "KfAwsPlugin" - GCP_PLUGIN_KIND PluginKindType = "KfGcpPlugin" - MINIKUBE_PLUGIN_KIND PluginKindType = "KfMinikubePlugin" - EXISTING_ARRIKTO_PLUGIN_KIND PluginKindType = "KfExistingArriktoPlugin" -) - // Plugin can be used to customize the generation and deployment of Kubeflow type Plugin struct { metav1.TypeMeta `json:",inline"` @@ -164,23 +141,6 @@ const ( KfDegraded KfDefConditionType = "Degraded" ) -// Define plugin related conditions to be the format: -// - conditions for successful plugins: ${PluginKind}Succeeded -// - conditions for failed plugins: ${PluginKind}Failed -func GetPluginSucceededCondition(pluginKind PluginKindType) KfDefConditionType { - return KfDefConditionType(fmt.Sprintf("%vSucceeded", pluginKind)) -} -func GetPluginFailedCondition(pluginKind PluginKindType) KfDefConditionType { - return KfDefConditionType(fmt.Sprintf("%vFailed", pluginKind)) -} - -type KfDefConditionReason string - -const ( - // InvalidKfDefSpecReason indicates the KfDef was not valid. - InvalidKfDefSpecReason KfDefConditionReason = "InvalidKfDefSpec" -) - type KfDefCondition struct { // Type of deployment condition. Type KfDefConditionType `json:"type"` @@ -195,203 +155,3 @@ type KfDefCondition struct { // A human readable message indicating details about the transition. Message string `json:"message,omitempty"` } - -// GetPluginSpec will try to unmarshal the spec for the specified plugin to the supplied -// interface. Returns an error if the plugin isn't defined or if there is a problem -// unmarshaling it. -func (d *KfDef) GetPluginSpec(pluginKind PluginKindType, s interface{}) error { - for _, p := range d.Spec.Plugins { - if p.Kind != string(pluginKind) { - continue - } - - // To deserialize it to a specific type we need to first serialize it to bytes - // and then unserialize it. - specBytes, err := yaml.Marshal(p.Spec) - - if err != nil { - msg := fmt.Sprintf("Could not marshal plugin %v args; error %v", pluginKind, err) - log.Errorf(msg) - return &kfapis.KfError{ - Code: int(kfapis.INVALID_ARGUMENT), - Message: msg, - } - } - - err = yaml.Unmarshal(specBytes, s) - - if err != nil { - msg := fmt.Sprintf("Could not unmarshal plugin %v to the provided type; error %v", pluginKind, err) - log.Errorf(msg) - return &kfapis.KfError{ - Code: int(kfapis.INTERNAL_ERROR), - Message: msg, - } - } - return nil - } - - return &kfapis.KfError{ - Code: int(kfapis.NOT_FOUND), - Message: fmt.Sprintf("%v %v", pluginNotFoundErrPrefix, pluginKind), - } -} - -// Sets condition and status to KfDef. -func (d *KfDef) SetCondition(condType KfDefConditionType, - status v1.ConditionStatus, - reason string, - message string) { - now := metav1.Now() - cond := KfDefCondition{ - Type: condType, - Status: status, - LastUpdateTime: now, - LastTransitionTime: now, - Reason: reason, - Message: message, - } - - for i := range d.Status.Conditions { - if d.Status.Conditions[i].Type != condType { - continue - } - if d.Status.Conditions[i].Status == status { - cond.LastTransitionTime = d.Status.Conditions[i].LastTransitionTime - } - d.Status.Conditions[i] = cond - return - } - d.Status.Conditions = append(d.Status.Conditions, cond) -} - -// Gets condition from KfDef. -func (d *KfDef) GetCondition(condType KfDefConditionType) (*KfDefCondition, error) { - for i := range d.Status.Conditions { - if d.Status.Conditions[i].Type == condType { - return &d.Status.Conditions[i], nil - } - } - return nil, &kfapis.KfError{ - Code: int(kfapis.NOT_FOUND), - Message: fmt.Sprintf("%v %v", conditionNotFoundErrPrefix, condType), - } -} - -// Check if a plugin is finished. -func (d *KfDef) IsPluginFinished(pluginKind PluginKindType) bool { - condType := GetPluginSucceededCondition(pluginKind) - cond, err := d.GetCondition(condType) - if err != nil { - log.Warnf("error when getting condition info: %v", err) - return false - } - return cond.Status == v1.ConditionTrue -} - -// Set a plugin as finished. -func (d *KfDef) SetPluginFinished(pluginKind PluginKindType, msg string) { - succeededCond := GetPluginSucceededCondition(pluginKind) - failedCond := GetPluginFailedCondition(pluginKind) - if _, err := d.GetCondition(failedCond); err == nil { - d.SetCondition(failedCond, v1.ConditionFalse, - "", "Reset to false as the plugin is set to be finished.") - } - - d.SetCondition(succeededCond, v1.ConditionTrue, "", msg) -} - -func (d *KfDef) IsPluginFailed(pluginKind PluginKindType) bool { - condType := GetPluginFailedCondition(pluginKind) - cond, err := d.GetCondition(condType) - if err != nil { - if IsConditionNotFound(err) { - return false - } - log.Warnf("error when getting condition info: %v", err) - return false - } - return cond.Status == v1.ConditionTrue -} - -func (d *KfDef) SetPluginFailed(pluginKind PluginKindType, msg string) { - succeededCond := GetPluginSucceededCondition(pluginKind) - failedCond := GetPluginFailedCondition(pluginKind) - if _, err := d.GetCondition(succeededCond); err == nil { - d.SetCondition(succeededCond, v1.ConditionFalse, - "", "Reset to false as the plugin is set to be failed.") - } - - d.SetCondition(failedCond, v1.ConditionTrue, "", msg) -} - -// SetPluginSpec sets the requested parameter. The plugin is added if it doesn't already exist. -func (d *KfDef) SetPluginSpec(pluginKind PluginKindType, spec interface{}) error { - // Convert spec to RawExtension - r := &runtime.RawExtension{} - - // To deserialize it to a specific type we need to first serialize it to bytes - // and then unserialize it. - specBytes, err := yaml.Marshal(spec) - - if err != nil { - msg := fmt.Sprintf("Could not marshal spec; error %v", err) - log.Errorf(msg) - return &kfapis.KfError{ - Code: int(kfapis.INVALID_ARGUMENT), - Message: msg, - } - } - - err = yaml.Unmarshal(specBytes, r) - - if err != nil { - msg := fmt.Sprintf("Could not unmarshal plugin to RawExtension; error %v", err) - log.Errorf(msg) - return &kfapis.KfError{ - Code: int(kfapis.INTERNAL_ERROR), - Message: msg, - } - } - - index := -1 - - for i, p := range d.Spec.Plugins { - if p.Kind == string(pluginKind) { - index = i - break - } - } - - if index == -1 { - // Plugin in doesn't exist so add it - log.Infof("Adding plugin %v", pluginKind) - - p := Plugin{} - p.Name = string(pluginKind) - p.Kind = string(pluginKind) - d.Spec.Plugins = append(d.Spec.Plugins, p) - - index = len(d.Spec.Plugins) - 1 - } - - d.Spec.Plugins[index].Spec = r - return nil -} - -func IsPluginNotFound(e error) bool { - if e == nil { - return false - } - err, ok := e.(*kfapis.KfError) - return ok && err.Code == int(kfapis.NOT_FOUND) && strings.HasPrefix(err.Message, pluginNotFoundErrPrefix) -} - -func IsConditionNotFound(e error) bool { - if e == nil { - return false - } - err, ok := e.(*kfapis.KfError) - return ok && err.Code == int(kfapis.NOT_FOUND) && - strings.HasPrefix(err.Message, conditionNotFoundErrPrefix) -} diff --git a/bootstrap/pkg/apis/apps/kfdef/v1beta1/application_types_test.go b/bootstrap/pkg/apis/apps/kfdef/v1beta1/application_types_test.go deleted file mode 100644 index 71e1eabb040..00000000000 --- a/bootstrap/pkg/apis/apps/kfdef/v1beta1/application_types_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package v1beta1 - -import ( - "github.com/ghodss/yaml" - kfutils "github.com/kubeflow/kubeflow/bootstrap/v3/pkg/utils" - "github.com/prometheus/common/log" - "io/ioutil" - "k8s.io/api/core/v1" - "os" - "path" - "reflect" - "testing" -) - -type FakeOAuthSecret struct { - Name string `json:"name,omitempty"` -} - -type FakeIap struct { - OAuthClientId string `json:"oAuthClientId,omitempty"` - OAuthClientSecret FakeOAuthSecret `json:"oAuthClientSecret,omitempty"` -} - -type FakeAuth struct { - Iap FakeIap `json:"iap,omitempty"` -} - -type GcpFakePluginSpec struct { - Auth FakeAuth `json:"auth,omitempty"` - CreatePipelinePersistentStorage bool `json:"createPipelinePersistentStorage,omitempty"` -} - -func TestKfDef_GetPluginSpec(t *testing.T) { - // Test that we can properly parse the gcp structs. - type testCase struct { - Filename string - PluginKind string - Expected *GcpFakePluginSpec - } - - cases := []testCase{ - { - Filename: "kfctl_plugin_test.yaml", - PluginKind: "kfGcpPlugin", - Expected: &GcpFakePluginSpec{ - Auth: FakeAuth{ - Iap: FakeIap{ - OAuthClientId: "foo-user", - OAuthClientSecret: FakeOAuthSecret{ - Name: "CLIENT_SECRET", - }, - }, - }, - CreatePipelinePersistentStorage: true, - }, - }, - } - - for _, c := range cases { - wd, _ := os.Getwd() - fPath := path.Join(wd, "testdata", c.Filename) - - buf, bufErr := ioutil.ReadFile(fPath) - if bufErr != nil { - t.Fatalf("Error reading file %v; error %v", fPath, bufErr) - } - - log.Infof("Want ") - d := &KfDef{} - err := yaml.Unmarshal(buf, d) - if err != nil { - t.Fatalf("Could not parse as KfDef error %v", err) - } - - actual := &GcpFakePluginSpec{} - err = d.GetPluginSpec(PluginKindType(c.PluginKind), actual) - - if err != nil { - t.Fatalf("Could not get plugin spec; error %v", err) - } - - if !reflect.DeepEqual(actual, c.Expected) { - pGot := kfutils.PrettyPrint(actual) - pWant := kfutils.PrettyPrint(c.Expected) - t.Errorf("Error parsing plugin spec got;\n%v\nwant;\n%v", pGot, pWant) - } - } -} - -func TestKfDef_ConditionStatus(t *testing.T) { - type testCase struct { - Type KfDefConditionType - Status v1.ConditionStatus - Name string - Reason string - Message string - } - - cases := []testCase{ - testCase{ - Type: GetPluginSucceededCondition(AWS_PLUGIN_KIND), - Status: v1.ConditionFalse, - Message: "foo status", - }, - testCase{ - Type: GetPluginSucceededCondition(GCP_PLUGIN_KIND), - Status: v1.ConditionTrue, - Message: "bar status", - }, - } - - for _, c := range cases { - kfdef := KfDef{} - kfdef.SetCondition(c.Type, c.Status, c.Reason, c.Message) - - cond, err := kfdef.GetCondition(c.Type) - if err != nil { - t.Fatalf("error when getting condition: %v", err) - } - if cond == nil { - t.Fatalf("condition returned is nil") - } - if cond.Type != c.Type || cond.Status != c.Status || - cond.Reason != c.Reason || cond.Message != c.Message { - t.Errorf("condition returned doesn't match the condition set;\nexpected\n%v\ngot\n%v", - kfutils.PrettyPrint(c), - kfutils.PrettyPrint(cond)) - } - } -} - -func TestKfDef_SetPluginStatus(t *testing.T) { - type testCase struct { - PluginKind string - Failed bool - Expected bool - } - - cases := []testCase{ - testCase{ - PluginKind: "KfGcpPlugin", - Failed: false, - Expected: true, - }, - testCase{ - PluginKind: "KfExistingArriktoPlugin", - Failed: false, - Expected: true, - }, - testCase{ - PluginKind: "KfMinikubePlugin", - Failed: true, - Expected: true, - }, - } - - for _, c := range cases { - kfdef := KfDef{} - if c.Failed { - kfdef.SetPluginFailed(PluginKindType(c.PluginKind), "") - } else { - kfdef.SetPluginFinished(PluginKindType(c.PluginKind), "") - } - actual := false - if c.Failed { - actual = kfdef.IsPluginFailed(PluginKindType(c.PluginKind)) - } else { - actual = kfdef.IsPluginFinished(PluginKindType(c.PluginKind)) - } - if actual != c.Expected { - t.Errorf("IsPluginFinished doesn't returned expected value; expected %v; got %v", - c.Expected, actual) - } - } -}