diff --git a/.gitignore b/.gitignore index 7c04eea..b084e08 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -overlays/ dist/ PROJECT.yaml diff --git a/PROJECT.example.yaml b/PROJECT.example.yaml new file mode 100644 index 0000000..1bf9e26 --- /dev/null +++ b/PROJECT.example.yaml @@ -0,0 +1,76 @@ +addons: + cluster-policies: + defaultEnabled: true + group: cluster-configs/policies + path: _example/source/addons/cluster-policies + disco-operator: + defaultEnabled: true + group: cluster-configs/policies + path: _example/source/addons/disco-operator + kyverno: + defaultEnabled: true + group: cluster-configs + path: _example/source/addons/kyverno + monitoring: + defaultEnabled: false + group: cluster-configs + path: _example/source/addons/monitoring +basePath: _example/overlays +environments: + dev: + actions: + postCreateHooks: null + postUpdateHooks: null + preCreateHooks: null + preUpdateHooks: null + addons: + cluster-policies: + enabled: true + properties: + enableNetworkPolicies: true + disco-operator: + enabled: true + properties: + isSuperCool: true + requiredDefaultNotSet: 10 + second: Hello World + properties: + gitBranch: develop + gitURL: https://github.com/leonsteinhaeuser/openshift-gitops-cli.git + stages: + dev: + actions: + postCreateHooks: null + postUpdateHooks: null + preCreateHooks: null + preUpdateHooks: null + addons: + cluster-policies: + enabled: true + properties: + enableNetworkPolicies: false + clusters: + hugi: + addons: + cluster-policies: + enabled: true + properties: + enableNetworkPolicies: true + disco-operator: + enabled: true + properties: + isSuperCool: false + requiredDefaultNotSet: null + second: Hello World + kyverno: + enabled: true + properties: {} + monitoring: + enabled: true + properties: + ingress_host: https://monitoring.2.external.url + properties: + destinationNamespace: openshift-gitops + properties: + destinationServer: https://kubernetes.default.svc +templateBasePath: _example/source/templates diff --git a/README.md b/README.md index 38c4f92..813c775 100644 --- a/README.md +++ b/README.md @@ -246,3 +246,4 @@ No matter if you define an addon or a template, you always have access to the fo | `{{ .Stage }}` | addon, tempate | The stage variable returns the name of the stage we are currently in | | `{{ .Cluster }}` | addon, tempate | The cluster variable returns the name of the cluster we are currently in | | `{{ .Properties. }}` | addon, tempate | The properties variable returns the value of the property with the key ``. The property keys in addons differ from the property keys in the template, as the addon does not currently have access to the environment, stage or cluster properties. In order for the addon to have properties available, you must define a property key in the `manifest.yaml` file. All properties defined there are then available for your addon template files. | +| `{{ .ClusterProperties. }}` | addon | The cluster properties is a map that contains all properties that are defined for the cluster. | diff --git a/_example/README.md b/_example/README.md new file mode 100644 index 0000000..244834b --- /dev/null +++ b/_example/README.md @@ -0,0 +1,3 @@ +# Example + +This folder contains an example for the openshift-gitops-cli. The `PROJECT.example.yaml` file contains the configuration for the example project. The `PROJECT.example.yaml` file is an example project config referencing the templates and addons in the `_example/source` folder. The `_example/overlys` folder contains the rendered configuration defined by the cluster defined in the PROJECT.example.yaml file. diff --git a/_example/overlays/dev/dev/hugi/app-of-apps/kustomization.yaml b/_example/overlays/dev/dev/hugi/app-of-apps/kustomization.yaml new file mode 100644 index 0000000..c6e223c --- /dev/null +++ b/_example/overlays/dev/dev/hugi/app-of-apps/kustomization.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +helmGlobals: + chartHome: ../../../../../../charts/ +helmCharts: + - name: argocd-app-of-app + version: 0.4.0 + valuesFile: values.yaml + namespace: openshift-gitops + releaseName: argocd-app-of-app-0.4.0 diff --git a/_example/overlays/dev/dev/hugi/app-of-apps/values.yaml b/_example/overlays/dev/dev/hugi/app-of-apps/values.yaml new file mode 100644 index 0000000..170b913 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/app-of-apps/values.yaml @@ -0,0 +1,68 @@ +--- +appSuffix: "hugi-dev" +appSourceBasePath: overlays/dev/dev/hugi/cluster-configs + +default: + app: + annotations: + argocd.argoproj.io/compare-options: IgnoreExtraneous + enabled: true + enableAutoSync: true + autoSyncPrune: true + project: hub + destination: + namespace: openshift-gitops + server: https://kubernetes.default.svc + source: + repoURL: https://github.com/leonsteinhaeuser/openshift-gitops-cli.git + targetRevision: + +projects: + hub: + annotations: + argocd.argoproj.io/sync-wave: "-2" + description: Project for cluster hub + namespace: openshift-gitops + sourceRepos: + - https://github.com/leonsteinhaeuser/openshift-gitops-cli.git + destinations: | + - namespace: '*' + server: https://kubernetes.default.svc + extraFields: | + clusterResourceWhitelist: + - group: '*' + kind: '*' + +applications: + cluster-policies: + enabled: true + annotations: + argocd.argoproj.io/sync-wave: "0" + source: + path: cluster-policies + labels: + app.kubernetes.io/managed-by: argocd + disco-operator: + enabled: true + annotations: + argocd.argoproj.io/sync-wave: "0" + source: + path: disco-operator + labels: + app.kubernetes.io/managed-by: argocd + kyverno: + enabled: true + annotations: + argocd.argoproj.io/sync-wave: "0" + source: + path: kyverno + labels: + app.kubernetes.io/managed-by: argocd + monitoring: + enabled: true + annotations: + argocd.argoproj.io/sync-wave: "0" + source: + path: monitoring + labels: + app.kubernetes.io/managed-by: argocd diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/kyverno/kustomization.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/kyverno/kustomization.yaml new file mode 100644 index 0000000..b7e65e5 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/kyverno/kustomization.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - "../../../../../../base/kyverno/" diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/kyverno/something.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/kyverno/something.yaml new file mode 100644 index 0000000..0c46315 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/kyverno/something.yaml @@ -0,0 +1 @@ +value: pair \ No newline at end of file diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/monitoring/kustomization.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/monitoring/kustomization.yaml new file mode 100644 index 0000000..0883773 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/monitoring/kustomization.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - "../../../../../../base/monitoring" diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/abc.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/abc.yaml new file mode 100644 index 0000000..7276569 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/abc.yaml @@ -0,0 +1,2 @@ +test: + hello: world diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/abc2.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/abc2.yaml new file mode 100644 index 0000000..7276569 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/abc2.yaml @@ -0,0 +1,2 @@ +test: + hello: world diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/abc.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/abc.yaml new file mode 100644 index 0000000..7276569 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/abc.yaml @@ -0,0 +1,2 @@ +test: + hello: world diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/sub/abc2.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/sub/abc2.yaml new file mode 100644 index 0000000..7276569 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/sub/abc2.yaml @@ -0,0 +1,2 @@ +test: + hello: world diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/sub/kustomization.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/sub/kustomization.yaml new file mode 100644 index 0000000..190cb8d --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/config/sub/kustomization.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - config/abc.yaml diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/kustomization.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/kustomization.yaml new file mode 100644 index 0000000..88616d9 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/cluster-policies/kustomization.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - config/abc.yaml + - "../../../../../../base/base-config/cluster-policies/" diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/disco-operator/kustomization.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/disco-operator/kustomization.yaml new file mode 100644 index 0000000..451929d --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/disco-operator/kustomization.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - "../../../../../../base/disco-operator/" +patches: +- name: Hello World + file: patch.yaml diff --git a/_example/overlays/dev/dev/hugi/cluster-configs/policies/disco-operator/patch.yaml b/_example/overlays/dev/dev/hugi/cluster-configs/policies/disco-operator/patch.yaml new file mode 100644 index 0000000..7fee6b0 --- /dev/null +++ b/_example/overlays/dev/dev/hugi/cluster-configs/policies/disco-operator/patch.yaml @@ -0,0 +1,3 @@ +--- +some: yaml +key: false diff --git a/_example/source/addons/cluster-policies/config/abc.yaml b/_example/source/addons/cluster-policies/config/abc.yaml new file mode 100644 index 0000000..7276569 --- /dev/null +++ b/_example/source/addons/cluster-policies/config/abc.yaml @@ -0,0 +1,2 @@ +test: + hello: world diff --git a/_example/source/addons/cluster-policies/config/sub/abc2.yaml b/_example/source/addons/cluster-policies/config/sub/abc2.yaml new file mode 100644 index 0000000..7276569 --- /dev/null +++ b/_example/source/addons/cluster-policies/config/sub/abc2.yaml @@ -0,0 +1,2 @@ +test: + hello: world diff --git a/_example/source/addons/cluster-policies/kustomization.yaml b/_example/source/addons/cluster-policies/kustomization.yaml new file mode 100644 index 0000000..3209548 --- /dev/null +++ b/_example/source/addons/cluster-policies/kustomization.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - config/abc.yaml + {{- if .Properties.enableNetworkPolicies}} + - "../../../../../../base/base-config/cluster-policies/" + {{- end}} diff --git a/_example/source/addons/cluster-policies/manifest.yaml b/_example/source/addons/cluster-policies/manifest.yaml new file mode 100644 index 0000000..c2c8acb --- /dev/null +++ b/_example/source/addons/cluster-policies/manifest.yaml @@ -0,0 +1,12 @@ +name: cluster-policies +group: cluster-policies +annotations: + argocd.argoproj.io/sync-wave: "0" +properties: + enableNetworkPolicies: + required: false + default: false + type: bool + description: "Whether to enable the cluster wide network policies" +files: + - ./ diff --git a/_example/source/addons/disco-operator/kustomization.yaml b/_example/source/addons/disco-operator/kustomization.yaml new file mode 100644 index 0000000..50f4641 --- /dev/null +++ b/_example/source/addons/disco-operator/kustomization.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - "../../../../../../base/disco-operator/" +patches: +- name: {{ .Properties.second }} + file: patch.yaml diff --git a/_example/source/addons/disco-operator/manifest.yaml b/_example/source/addons/disco-operator/manifest.yaml new file mode 100644 index 0000000..64f905f --- /dev/null +++ b/_example/source/addons/disco-operator/manifest.yaml @@ -0,0 +1,23 @@ +name: disco-operator +group: cluster-policies +annotations: + argocd.argoproj.io/sync-wave: "0" +properties: + isSuperCool: + required: false + default: false + type: bool + description: "Is this app super cool?" + second: + required: true + default: "Hello World" + type: string + description: "Second?" + requiredDefaultNotSet: + required: true + default: null + type: int + description: "An empty property that is required" +files: + - kustomization.yaml + - patch.yaml diff --git a/_example/source/addons/disco-operator/patch.yaml b/_example/source/addons/disco-operator/patch.yaml new file mode 100644 index 0000000..b2cec3f --- /dev/null +++ b/_example/source/addons/disco-operator/patch.yaml @@ -0,0 +1,3 @@ +--- +some: yaml +key: {{ .Properties.isSuperCool }} diff --git a/_example/source/addons/kyverno/kustomization.yaml b/_example/source/addons/kyverno/kustomization.yaml new file mode 100644 index 0000000..b7e65e5 --- /dev/null +++ b/_example/source/addons/kyverno/kustomization.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - "../../../../../../base/kyverno/" diff --git a/_example/source/addons/kyverno/manifest.yaml b/_example/source/addons/kyverno/manifest.yaml new file mode 100644 index 0000000..7f4bec1 --- /dev/null +++ b/_example/source/addons/kyverno/manifest.yaml @@ -0,0 +1,7 @@ +name: kyverno +group: cluster-policies +annotations: + argocd.argoproj.io/sync-wave: "0" +properties: {} +files: + - ./ diff --git a/_example/source/addons/kyverno/something.yaml b/_example/source/addons/kyverno/something.yaml new file mode 100644 index 0000000..0c46315 --- /dev/null +++ b/_example/source/addons/kyverno/something.yaml @@ -0,0 +1 @@ +value: pair \ No newline at end of file diff --git a/_example/source/addons/monitoring/kustomization.yaml b/_example/source/addons/monitoring/kustomization.yaml new file mode 100644 index 0000000..0883773 --- /dev/null +++ b/_example/source/addons/monitoring/kustomization.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - "../../../../../../base/monitoring" diff --git a/_example/source/addons/monitoring/manifest.yaml b/_example/source/addons/monitoring/manifest.yaml new file mode 100644 index 0000000..7d4eebc --- /dev/null +++ b/_example/source/addons/monitoring/manifest.yaml @@ -0,0 +1,12 @@ +name: cluster-policies +group: cluster-policies +annotations: + argocd.argoproj.io/sync-wave: "0" +properties: + ingress_host: + required: false + default: false + type: string + description: "The host to expose grafana on" +files: + - ./ diff --git a/_example/source/templates/appofapps/kustomization.yaml b/_example/source/templates/appofapps/kustomization.yaml new file mode 100644 index 0000000..c6e223c --- /dev/null +++ b/_example/source/templates/appofapps/kustomization.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +helmGlobals: + chartHome: ../../../../../../charts/ +helmCharts: + - name: argocd-app-of-app + version: 0.4.0 + valuesFile: values.yaml + namespace: openshift-gitops + releaseName: argocd-app-of-app-0.4.0 diff --git a/_example/source/templates/appofapps/manifest.yaml b/_example/source/templates/appofapps/manifest.yaml new file mode 100644 index 0000000..877b490 --- /dev/null +++ b/_example/source/templates/appofapps/manifest.yaml @@ -0,0 +1,13 @@ +name: app-of-apps +properties: + gitURL: + required: true + default: "" + descriptionL: "Please define the git URL ArgoCD should reference" + targetRevision: + required: false + default: "develop" + description: "Please define the git target revision ArgoCD should reference" +files: + - values.yaml + - kustomization.yaml diff --git a/_example/source/templates/appofapps/values.yaml b/_example/source/templates/appofapps/values.yaml new file mode 100644 index 0000000..b1d51c1 --- /dev/null +++ b/_example/source/templates/appofapps/values.yaml @@ -0,0 +1,46 @@ +--- +appSuffix: "{{ .ClusterName }}-{{ .Stage }}" +appSourceBasePath: overlays/{{ .Environment }}/{{ .Stage }}/{{ .ClusterName }}/cluster-configs + +default: + app: + annotations: + argocd.argoproj.io/compare-options: IgnoreExtraneous + enabled: true + enableAutoSync: true + autoSyncPrune: true + project: hub + destination: + namespace: {{ .Properties.destinationNamespace }} + server: {{ .Properties.destinationServer }} + source: + repoURL: {{ .Properties.gitURL }} + targetRevision: {{ .Properties.gitTargetRevision }} + +projects: + hub: + annotations: + argocd.argoproj.io/sync-wave: "-2" + description: Project for cluster hub + namespace: openshift-gitops + sourceRepos: + - {{ .Properties.gitURL }} + destinations: | + - namespace: '*' + server: {{ .Properties.destinationServer }} + extraFields: | + clusterResourceWhitelist: + - group: '*' + kind: '*' + +applications: + {{- range $key, $value := .Addons }} + {{ $key }}: + enabled: {{ $value.Enabled }} + annotations: + {{- $value.Annotations | toYaml | nindent 6 }} + source: + path: {{ $key }} + labels: + app.kubernetes.io/managed-by: argocd + {{- end }} diff --git a/cmd/main.go b/cmd/main.go index 2a2f0a0..79f5c4d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -117,7 +117,7 @@ func main() { } if event.Environment != "" && event.Stage == "" && event.Cluster == "" { - env := projectConfig.Environments[event.Environment] + env := projectConfig.GetEnvironment(event.Environment) err := executeHook(os.Stdout, os.Stderr, event.Type, event.Runtime, env.Actions) if err != nil { fmt.Println(err) @@ -126,7 +126,7 @@ func main() { } if event.Environment != "" && event.Stage != "" && event.Cluster == "" { - stage := projectConfig.Environments[event.Environment].Stages[event.Stage] + stage := projectConfig.GetStage(event.Environment, event.Stage) err := executeHook(os.Stdout, os.Stderr, event.Type, event.Runtime, stage.Actions) if err != nil { fmt.Println(err) @@ -135,7 +135,7 @@ func main() { } if event.Environment != "" && event.Stage != "" && event.Cluster != "" { - cluster := projectConfig.Cluster(event.Environment, event.Stage, event.Cluster) + cluster := projectConfig.GetCluster(event.Environment, event.Stage, event.Cluster) if event.Type == menu.EventTypeCreate || event.Type == menu.EventTypeUpdate { err := cluster.Render(projectConfig, event.Environment, event.Stage) if err != nil { diff --git a/go.mod b/go.mod index 3b6fa08..1795930 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.23.2 require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/google/go-cmp v0.7.0 - github.com/k0kubun/pp/v3 v3.4.1 github.com/manifoldco/promptui v0.9.0 sigs.k8s.io/yaml v1.4.0 ) @@ -17,13 +16,10 @@ require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/google/uuid v1.6.0 // indirect github.com/huandu/xstrings v1.5.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.7.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect ) diff --git a/go.sum b/go.sum index 057e65c..2dace1f 100644 --- a/go.sum +++ b/go.sum @@ -23,18 +23,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/k0kubun/pp/v3 v3.4.1 h1:1WdFZDRRqe8UsR61N/2RoOZ3ziTEqgTPVqKrHeb779Y= -github.com/k0kubun/pp/v3 v3.4.1/go.mod h1:+SiNiqKnBfw1Nkj82Lh5bIeKQOAkPy6Xw9CAZUZ8npI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -52,11 +46,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= diff --git a/internal/menu/addon.go b/internal/menu/addon.go index cff7917..5db65af 100644 --- a/internal/menu/addon.go +++ b/internal/menu/addon.go @@ -17,12 +17,12 @@ type addonClusterMenu struct { config *project.ProjectConfig } -func (a *addonClusterMenu) menuManageAddons(cluster *project.Cluster) error { +func (a *addonClusterMenu) menuManageAddons(ah project.AddonHandler, skipAddonValidation bool) error { for { prompt := promptui.Select{ Label: "Manage Addons", Items: append(utils.SortStringSlice(utils.MapKeysToList(a.config.ParsedAddons)), "Done"), - Templates: a.templateManageAddons(cluster), + Templates: a.templateManageAddons(ah), Size: 10, } _, result, err := prompt.Run() @@ -30,15 +30,16 @@ func (a *addonClusterMenu) menuManageAddons(cluster *project.Cluster) error { return err } if result == "Done" { - err := cluster.AllRequiredPropertiesSet(a.config) + err := ah.GetAddons().AllRequiredPropertiesSet(a.config, skipAddonValidation) if err != nil { fmt.Println("Not all required properties are set", err) continue } + fmt.Println("Done managing addons") break } - err = a.menuAddonSettings(cluster, result) + err = a.menuAddonSettings(ah, result) if err != nil { return err } @@ -46,7 +47,7 @@ func (a *addonClusterMenu) menuManageAddons(cluster *project.Cluster) error { return nil } -func (a *addonClusterMenu) templateManageAddons(cluster *project.Cluster) *promptui.SelectTemplates { +func (a *addonClusterMenu) templateManageAddons(ah project.AddonHandler) *promptui.SelectTemplates { return &promptui.SelectTemplates{ Label: "{{ . }}", Details: "{{ addon . }}", @@ -59,7 +60,7 @@ func (a *addonClusterMenu) templateManageAddons(cluster *project.Cluster) *promp resultString := "--------------------------------\nDetails:\n" resultString += fmt.Sprintf("\tDescription: %s\n", a.config.ParsedAddons[addonName].Description) resultString += fmt.Sprintf("\tGroup: %s\n", a.config.ParsedAddons[addonName].Group) - resultString += fmt.Sprintf("\tEnabled: %v | Default: %v\n", cluster.IsAddonEnabled(addonName), a.config.Addons[addonName].DefaultEnabled) + resultString += fmt.Sprintf("\tEnabled: %v | Default: %v\n", ah.GetAddon(addonName).IsEnabled(), a.config.Addons[addonName].DefaultEnabled) return resultString } return funcmap @@ -67,10 +68,10 @@ func (a *addonClusterMenu) templateManageAddons(cluster *project.Cluster) *promp } } -func (a *addonClusterMenu) menuAddonSettings(cluster *project.Cluster, addon string) error { +func (a *addonClusterMenu) menuAddonSettings(ah project.AddonHandler, addon string) error { for { selectOptions := []string{"Enable", "Done"} - if (*cluster).IsAddonEnabled(addon) { + if ah.IsAddonEnabled(addon) { selectOptions = []string{"Disable", "Settings", "Done"} } @@ -85,25 +86,23 @@ func (a *addonClusterMenu) menuAddonSettings(cluster *project.Cluster, addon str switch result { case "Enable": - fmt.Println("Enable addon", addon) - cluster.EnableAddon(addon) + ah.EnableAddon(addon) case "Disable": - fmt.Println("Disable addon", addon) - cluster.DisableAddon(addon) + ah.DisableAddon(addon) case "Settings": - err := a.menuAddonProperties(cluster, addon) + err := a.menuAddonProperties(ah, addon) if err != nil { return err } case "Done": - if !(*cluster).IsAddonEnabled(addon) { + if !ah.IsAddonEnabled(addon) { return nil } // check if all required properties are set - err := cluster.Addons[addon].AllRequiredPropertiesSet(a.config, addon) + err := ah.GetAddon(addon).AllRequiredPropertiesSet(a.config, addon) if err != nil { - fmt.Println("Not all required properties are set", err) + fmt.Println(utils.Red.Wrap("Not all required properties are set"), err) continue } return nil @@ -113,13 +112,13 @@ func (a *addonClusterMenu) menuAddonSettings(cluster *project.Cluster, addon str } } -func (a *addonClusterMenu) menuAddonProperties(cluster *project.Cluster, addon string) error { +func (a *addonClusterMenu) menuAddonProperties(ah project.AddonHandler, addon string) error { for { prompt := promptui.Select{ Label: "Properties", Items: append(utils.SortStringSlice(utils.MapKeysToList(a.config.ParsedAddons[addon].Properties)), "Done"), // TODO: add template to display property options - Templates: a.menuTemplateAddonProperties(cluster, addon), + Templates: a.menuTemplateAddonProperties(ah, addon), } _, result, err := prompt.Run() if err != nil { @@ -129,32 +128,29 @@ func (a *addonClusterMenu) menuAddonProperties(cluster *project.Cluster, addon s break } - value, err := cli.UntypedQuestion(a.writer, a.reader, "Value", cluster.Addons[addon].Properties[result], func(s any) error { + value, err := cli.UntypedQuestion(a.writer, a.reader, "Value", fmt.Sprintf("%v", ah.GetAddon(addon).Properties[result]), func(s any) error { if s == nil { return fmt.Errorf("value cannot be empty") } return nil }) if err != nil { - fmt.Println("Value violates requirements, please try again", err) + fmt.Println(utils.Red.Wrap("Value violates requirements, please try again"), err) continue } - if cluster.Addons[addon].Properties == nil { - cluster.Addons[addon].Properties = map[string]any{} - } value, err = a.config.ParsedAddons[addon].Properties[result].ParseValue(value) if err != nil { - fmt.Println("Value violates requirements, please try again", err) + fmt.Println(utils.Red.Wrap("Value violates requirements, please try again"), err) continue } - cluster.Addons[addon].Properties[result] = value + ah.GetAddon(addon).SetProperty(result, value) } return nil } // menuTemplateAddonProperties returns a promptui.SelectTemplates for the addon properties -func (a *addonClusterMenu) menuTemplateAddonProperties(cluster *project.Cluster, addon string) *promptui.SelectTemplates { +func (a *addonClusterMenu) menuTemplateAddonProperties(ah project.AddonHandler, addon string) *promptui.SelectTemplates { return &promptui.SelectTemplates{ Label: "{{ . }}", Details: "{{ properties . }}", @@ -169,7 +165,7 @@ func (a *addonClusterMenu) menuTemplateAddonProperties(cluster *project.Cluster, resultString += fmt.Sprintf("\tRequired: %v\n", a.config.ParsedAddons[addon].Properties[selectValue].Required) resultString += fmt.Sprintf("\tType: %v\n", a.config.ParsedAddons[addon].Properties[selectValue].Type) resultString += fmt.Sprintf("\tDefault: %v\n", a.config.ParsedAddons[addon].Properties[selectValue].Default) - data := cluster.Addons[addon].Properties[selectValue] + data := ah.GetAddon(addon).Properties[selectValue] if data == nil { data = a.config.ParsedAddons[addon].Properties[selectValue].Default } diff --git a/internal/menu/cluster.go b/internal/menu/cluster.go index e8c9cfa..a4676a8 100644 --- a/internal/menu/cluster.go +++ b/internal/menu/cluster.go @@ -67,7 +67,7 @@ func (c *clusterMenu) menuSettings(env, stage string, cluster *project.Cluster) config: c.config, } - err := addon.menuManageAddons(cluster) + err := addon.menuManageAddons(cluster, true) if err != nil { return err } @@ -87,7 +87,7 @@ func (c *clusterMenu) menuSettings(env, stage string, cluster *project.Cluster) // menuUpdateCluster creates a context menu to update an existing cluster func (c *clusterMenu) menuUpdateCluster(envName, stageName, clusterName string) (*project.Cluster, error) { - cluster := c.config.Cluster(envName, stageName, clusterName) + cluster := c.config.GetCluster(envName, stageName, clusterName) if cluster.Name == "" { cluster.Name = clusterName } @@ -108,7 +108,7 @@ func (c *clusterMenu) menuDeleteCluster(env, stage, cluster string) (*project.Cl if !confirmation { return nil, fmt.Errorf("confirmation denied") } - return c.config.Cluster(env, stage, cluster), nil + return c.config.GetCluster(env, stage, cluster), nil } func (c *clusterMenu) menuClusterSettingsProperties(env, stage string, cluster *project.Cluster) (map[string]string, error) { diff --git a/internal/menu/environment.go b/internal/menu/environment.go index e422016..0799f4c 100644 --- a/internal/menu/environment.go +++ b/internal/menu/environment.go @@ -23,7 +23,7 @@ func (e *environmentMenu) menuCreateEnvironment() (*project.Environment, error) if s == "" { return fmt.Errorf("environment name cannot be empty") } - if _, ok := e.config.Environments[s]; ok { + if e.config.HasEnvironment(s) { return fmt.Errorf("environment already exists") } return nil @@ -36,28 +36,64 @@ func (e *environmentMenu) menuCreateEnvironment() (*project.Environment, error) Name: env, Stages: map[string]*project.Stage{}, Properties: map[string]string{}, + Addons: map[string]*project.ClusterAddon{}, } - // ask for properties - properties, err := e.menuEnvironmentProperties(environment) + err = e.menuSettings(environment) if err != nil { return nil, err } - environment.Properties = properties return environment, nil } func (e *environmentMenu) menuUpdateEnvironment(envName string) (*project.Environment, error) { - environment := *e.config.Environments[envName] - environment.Name = envName - // ask for properties - properties, err := e.menuEnvironmentProperties(&environment) + environment := e.config.GetEnvironment(envName) + if environment.Addons == nil { + environment.Addons = map[string]*project.ClusterAddon{} + } + err := e.menuSettings(environment) if err != nil { return nil, err } - environment.Properties = properties - return &environment, nil + return environment, nil +} + +// menuSettings creates a context menu to manage the settings of a cluster +func (e *environmentMenu) menuSettings(environment *project.Environment) error { + for { + prompt := promptui.Select{ + Label: "Settings", + Items: []string{"Addons", "Properties", "Done"}, + } + _, result, err := prompt.Run() + if err != nil { + return err + } + + switch result { + case "Addons": + addon := addonClusterMenu{ + writer: e.writer, + reader: e.reader, + config: e.config, + } + err := addon.menuManageAddons(environment, true) + if err != nil { + return err + } + case "Properties": + properties, err := e.menuEnvironmentProperties(environment) + if err != nil { + return err + } + environment.Properties = properties + case "Done": + return nil + default: + return fmt.Errorf("invalid option %s", result) + } + } } func (e *environmentMenu) menuDeleteEnvironment(envName string) (*project.Environment, error) { @@ -68,19 +104,17 @@ func (e *environmentMenu) menuDeleteEnvironment(envName string) (*project.Enviro if !confirmation { return nil, fmt.Errorf("confirmation denied") } - environment := *e.config.Environments[envName] + environment := e.config.GetEnvironment(envName) environment.Name = envName - return &environment, errors.New("menuDeleteEnvironment not implemented") + return environment, errors.New("menuDeleteEnvironment not implemented") } func (e *environmentMenu) menuEnvironmentProperties(env *project.Environment) (map[string]string, error) { - envProperties := map[string]string{} + envProperties := env.Properties for { - properties := utils.MergeMaps(env.Properties, envProperties) - prompt := promptui.SelectWithAdd{ Label: "Properties", - Items: append(utils.SortStringSlice(utils.MapKeysToList(properties)), "Done"), + Items: append(utils.SortStringSlice(utils.MapKeysToList(envProperties)), "Done"), AddLabel: "Create Property", } _, result, err := prompt.Run() @@ -95,7 +129,7 @@ func (e *environmentMenu) menuEnvironmentProperties(env *project.Environment) (m break } - val, err := cli.StringQuestion(e.writer, e.reader, "Property Value", properties[result], func(s string) error { + val, err := cli.StringQuestion(e.writer, e.reader, "Property Value", envProperties[result], func(s string) error { if s == "" { return fmt.Errorf("property value cannot be empty") } diff --git a/internal/menu/root.go b/internal/menu/root.go index bb9a515..676a52c 100644 --- a/internal/menu/root.go +++ b/internal/menu/root.go @@ -67,7 +67,7 @@ func RootMenu(config *project.ProjectConfig, eventCh chan<- Event) error { } eventCh <- newPreUpdateEvent(EventOriginEnvironment, environment.Name, "", "") - config.Environments[*env].Properties = environment.Properties + config.GetEnvironment(environment.Name).Properties = environment.Properties eventCh <- newPostUpdateEvent(EventOriginEnvironment, environment.Name, "", "") return nil } @@ -110,7 +110,7 @@ func RootMenu(config *project.ProjectConfig, eventCh chan<- Event) error { eventCh <- newPreCreateEvent(EventOriginStage, *envName, stage.Name, "") // add stage to config - config.Environments[*envName].Stages[stage.Name] = stage + config.GetEnvironment(*envName).Stages[stage.Name] = stage eventCh <- newPostCreateEvent(EventOriginStage, *envName, stage.Name, "") return nil } @@ -128,9 +128,9 @@ func RootMenu(config *project.ProjectConfig, eventCh chan<- Event) error { eventCh <- newPreUpdateEvent(EventOriginStage, *envName, *stageName, "") // update stage in config - config.Environments[*envName].Stages[stage.Name] = stage + config.GetEnvironment(*envName).Stages[stage.Name] = stage eventCh <- newPostUpdateEvent(EventOriginStage, *envName, stage.Name, "") - return errors.New("stage update not implemented") + return nil } delete := func() error { @@ -295,7 +295,7 @@ func menuSelectEnvironment(config *project.ProjectConfig) (*string, error) { func menuSelectStage(config *project.ProjectConfig, environment string) (*string, error) { prompt := promptui.Select{ Label: "Select Stage", - Items: append(utils.MapKeysToList(config.Environments[environment].Stages), rootOptionDone), + Items: append(utils.MapKeysToList(config.GetEnvironment(environment).Stages), rootOptionDone), } _, result, err := prompt.Run() if err != nil { @@ -329,7 +329,7 @@ func menuHierarchySelectEnvironmentStage(config *project.ProjectConfig) (*string func menuSelectCluster(config *project.ProjectConfig, environment, stage string) (*string, error) { prompt := promptui.Select{ Label: "Select Cluster", - Items: append(utils.MapKeysToList(config.Environments[environment].Stages[stage].Clusters), rootOptionDone), + Items: append(utils.MapKeysToList(config.GetStage(environment, stage).Clusters), rootOptionDone), } _, result, err := prompt.Run() if err != nil { diff --git a/internal/menu/stage.go b/internal/menu/stage.go index d9966f4..0e4109b 100644 --- a/internal/menu/stage.go +++ b/internal/menu/stage.go @@ -23,7 +23,7 @@ func (s *stageMenu) menuCreateStage(env string) (*project.Stage, error) { if str == "" { return fmt.Errorf("stage name cannot be empty") } - if _, ok := s.config.Environments[env].Stages[str]; ok { + if s.config.GetEnvironment(env).HasStage(str) { return fmt.Errorf("stage already exists") } return nil @@ -37,27 +37,66 @@ func (s *stageMenu) menuCreateStage(env string) (*project.Stage, error) { Properties: map[string]string{}, Actions: project.Actions{}, Clusters: map[string]*project.Cluster{}, + Addons: map[string]*project.ClusterAddon{}, } - properties, err := s.menuProperties(stage) + err = s.menuSettings(stage) if err != nil { return nil, err } - stage.Properties = properties return stage, nil } func (s *stageMenu) menuUpdateStage(envName, stageName string) (*project.Stage, error) { - stage := s.config.Environments[envName].Stages[stageName] - properties, err := s.menuProperties(stage) + stage := s.config.GetStage(envName, stageName) + if stage.Addons == nil { + stage.Addons = map[string]*project.ClusterAddon{} + } + err := s.menuSettings(stage) if err != nil { return nil, err } - stage.Properties = properties return stage, nil } +// menuSettings creates a context menu to manage the settings of a cluster +func (s *stageMenu) menuSettings(stage *project.Stage) error { + for { + prompt := promptui.Select{ + Label: "Settings", + Items: []string{"Addons", "Properties", "Done"}, + } + _, result, err := prompt.Run() + if err != nil { + return err + } + + switch result { + case "Addons": + addon := addonClusterMenu{ + writer: s.writer, + reader: s.reader, + config: s.config, + } + err := addon.menuManageAddons(stage, true) + if err != nil { + return err + } + case "Properties": + properties, err := s.menuProperties(stage) + if err != nil { + return err + } + stage.Properties = properties + case "Done": + return nil + default: + return fmt.Errorf("invalid option %s", result) + } + } +} + func (s *stageMenu) menuDeleteStage(env, stage string) error { // TODO: menu is missing to delete the stage (cascade delete) return errors.New("not implemented") diff --git a/internal/project/addon.go b/internal/project/addon.go new file mode 100644 index 0000000..5d5d743 --- /dev/null +++ b/internal/project/addon.go @@ -0,0 +1,72 @@ +package project + +import ( + "fmt" +) + +type AddonHandler interface { + // IsAddonEnabled checks if the addon is enabled + IsAddonEnabled(name string) bool + // EnableAddon enables the addon + EnableAddon(name string) + // DisableAddon disables the addon + DisableAddon(name string) + // GetAddons returns the addons + GetAddons() ClusterAddons + // GetAddon returns the addon by name + GetAddon(name string) *ClusterAddon +} + +type ClusterAddons map[string]*ClusterAddon + +func (ca ClusterAddons) AllRequiredPropertiesSet(config *ProjectConfig, skipOnFailure bool) error { + for addonName, addon := range ca { + if !addon.Enabled { + fmt.Printf("addon %s is disabled\n", addonName) + continue + } + err := addon.AllRequiredPropertiesSet(config, addonName) + if err != nil && !skipOnFailure { + return fmt.Errorf("failed to validate addon %s: %w", addonName, err) + } + } + return nil +} + +func (ca ClusterAddons) IsEnabled(addon string) bool { + if ca[addon] == nil { + return false + } + return ca[addon].IsEnabled() +} + +type ClusterAddon struct { + Enabled bool `json:"enabled"` + Properties map[string]any `json:"properties"` +} + +// AllRequiredPropertiesSet checks if all required properties are set for the addon +func (ca *ClusterAddon) AllRequiredPropertiesSet(config *ProjectConfig, addonName string) error { + for key, property := range config.ParsedAddons[addonName].Properties { + if property.Required && ca.Properties[key] == nil { + return fmt.Errorf("[%s] property for key %s is required", addonName, key) + } + _, err := property.ParseValue(ca.Properties[key]) + if err != nil { + return fmt.Errorf("[%s] property for key %s is invalid: %w", addonName, key, err) + } + } + return nil +} + +// IsEnabled checks if the addon is enabled +func (ca ClusterAddon) IsEnabled() bool { + return ca.Enabled +} + +func (ca *ClusterAddon) SetProperty(key string, value any) { + if ca.Properties == nil { + ca.Properties = map[string]any{} + } + ca.Properties[key] = value +} diff --git a/internal/project/addon_test.go b/internal/project/addon_test.go new file mode 100644 index 0000000..06995c9 --- /dev/null +++ b/internal/project/addon_test.go @@ -0,0 +1,866 @@ +package project + +import ( + "testing" + + "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/template" +) + +func TestClusterAddons_AllRequiredPropertiesSet(t *testing.T) { + type fields struct { + Addons ClusterAddons + } + type args struct { + config *ProjectConfig + skipOnFailure bool + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "no addons defined", + fields: fields{ + Addons: ClusterAddons{}, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: false, + }, + { + name: "one property set", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": true, + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: false, + }, + { + name: "one property set string, expect bool", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "invalid", + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: true, + }, + { + name: "one property set int, expect bool", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": 10, + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: true, + }, + { + name: "one property set bool, expect string", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": true, + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: true, + }, + { + name: "one property set int, expect string", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": 10, + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: true, + }, + { + name: "one property set string, expect string", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "value", + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: false, + }, + { + name: "one property set int, expect int", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": 10, + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: false, + }, + { + name: "one property set string, expect int", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "value", + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: true, + }, + { + name: "one property set int, expect int", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": 10, + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: false, + }, + { + name: "one property set int, expect int, invalid value", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "invalid", + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: true, + }, + { + name: "one property set int, expect int, invalid value", + fields: fields{ + Addons: ClusterAddons{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "invalid", + }, + }, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + skipOnFailure: false, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ca := tt.fields.Addons + if err := ca.AllRequiredPropertiesSet(tt.args.config, tt.args.skipOnFailure); (err != nil) != tt.wantErr { + t.Errorf("ClusterAddons.AllRequiredPropertiesSet() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestClusterAddon_AllRequiredPropertiesSet(t *testing.T) { + type fields struct { + Enabled bool + Properties map[string]any + } + type args struct { + config *ProjectConfig + addonName string + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "no properties defined in addon properties", + fields: fields{ + Properties: map[string]any{}, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: true, + }, + { + name: "one property set", + fields: fields{ + Properties: map[string]any{ + "property1": true, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: false, + }, + { + name: "one property set string, expect bool", + fields: fields{ + Properties: map[string]any{ + "property1": "invalid", + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: true, + }, + { + name: "one property set int, expect bool", + fields: fields{ + Properties: map[string]any{ + "property1": 10, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: true, + }, + { + name: "one property set bool, expect string", + fields: fields{ + Properties: map[string]any{ + "property1": true, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: true, + }, + { + name: "one property set int, expect string", + fields: fields{ + Properties: map[string]any{ + "property1": 10, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: true, + }, + { + name: "one property set string, expect string", + fields: fields{ + Properties: map[string]any{ + "property1": "value", + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: false, + }, + { + name: "one property set int, expect int", + fields: fields{ + Properties: map[string]any{ + "property1": 10, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: false, + }, + { + name: "one property set string, expect int", + fields: fields{ + Properties: map[string]any{ + "property1": "value", + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: true, + }, + { + name: "one property set int, expect int", + fields: fields{ + Properties: map[string]any{ + "property1": 10, + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: false, + }, + { + name: "one property set int, expect int, invalid value", + fields: fields{ + Properties: map[string]any{ + "property1": "invalid", + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: true, + }, + { + name: "one property set int, expect int, invalid value", + fields: fields{ + Properties: map[string]any{ + "property1": "invalid", + }, + }, + args: args{ + config: &ProjectConfig{ + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + addonName: "addon1", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ca := &ClusterAddon{ + Enabled: tt.fields.Enabled, + Properties: tt.fields.Properties, + } + if err := ca.AllRequiredPropertiesSet(tt.args.config, tt.args.addonName); (err != nil) != tt.wantErr { + t.Errorf("ClusterAddon.AllRequiredPropertiesSet() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestClusterAddons_IsEnabled(t *testing.T) { + type args struct { + addon string + } + tests := []struct { + name string + ca ClusterAddons + args args + want bool + }{ + { + name: "addon not enabled", + ca: ClusterAddons{ + "addon1": { + Enabled: false, + }, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "addon enabled", + ca: ClusterAddons{ + "addon1": { + Enabled: true, + }, + }, + args: args{ + addon: "addon1", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.ca.IsEnabled(tt.args.addon); got != tt.want { + t.Errorf("ClusterAddons.IsEnabled() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestClusterAddon_IsEnabled(t *testing.T) { + type fields struct { + Enabled bool + Properties map[string]any + } + tests := []struct { + name string + fields fields + want bool + }{ + { + name: "addon not enabled", + fields: fields{ + Enabled: false, + }, + want: false, + }, + { + name: "addon enabled", + fields: fields{ + Enabled: true, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ca := ClusterAddon{ + Enabled: tt.fields.Enabled, + Properties: tt.fields.Properties, + } + if got := ca.IsEnabled(); got != tt.want { + t.Errorf("ClusterAddon.IsEnabled() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestClusterAddon_SetProperty(t *testing.T) { + type fields struct { + Enabled bool + Properties map[string]any + } + type args struct { + key string + value any + } + tests := []struct { + name string + fields fields + args args + }{ + { + name: "set property", + fields: fields{ + Properties: map[string]any{}, + }, + args: args{ + key: "property1", + value: "value", + }, + }, + { + name: "set property, override", + fields: fields{ + Properties: map[string]any{ + "property1": "value", + }, + }, + args: args{ + key: "property1", + value: "value2", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ca := &ClusterAddon{ + Enabled: tt.fields.Enabled, + Properties: tt.fields.Properties, + } + ca.SetProperty(tt.args.key, tt.args.value) + }) + } +} diff --git a/internal/project/cluster.go b/internal/project/cluster.go index ba9edec..dbf3d42 100644 --- a/internal/project/cluster.go +++ b/internal/project/cluster.go @@ -7,24 +7,9 @@ import ( "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/utils" ) -type ClusterAddon struct { - Enabled bool `json:"enabled"` - Properties map[string]any `json:"properties"` -} - -// AllRequiredPropertiesSet checks if all required properties are set for the addon -func (ca *ClusterAddon) AllRequiredPropertiesSet(config *ProjectConfig, addonName string) error { - for key, property := range config.ParsedAddons[addonName].Properties { - if property.Required && ca.Properties[key] == nil { - return fmt.Errorf("property for key %s is required", key) - } - _, err := config.ParsedAddons[addonName].Properties[key].ParseValue(ca.Properties[key]) - if err != nil { - return fmt.Errorf("property for key %s is invalid: %w", key, err) - } - } - return nil -} +var ( + _ AddonHandler = &Cluster{} +) type Cluster struct { Name string `json:"-"` @@ -50,10 +35,9 @@ func (c *Cluster) EnableAddon(addon string) { return } c.Addons[addon].Enabled = true - fmt.Println("Enabled addon option:", c.Addons[addon].Enabled) } -// DisableAddon disables the addon for the cluster by setting the enabled flag to false and removing all properties +// DisableAddon disables the addon for the cluster by setting the enabled flag to false func (c *Cluster) DisableAddon(addon string) { if _, ok := c.Addons[addon]; !ok { // already disabled or not found @@ -62,6 +46,16 @@ func (c *Cluster) DisableAddon(addon string) { c.Addons[addon].Enabled = false } +// GetAddons returns the cluster addons +func (c *Cluster) GetAddons() ClusterAddons { + return c.Addons +} + +// GetAddon returns the addon by name +func (c *Cluster) GetAddon(name string) *ClusterAddon { + return c.Addons[name] +} + // Render renders the cluster configuration using the given project templates func (c *Cluster) Render(config *ProjectConfig, env, stage string) error { properties := utils.MergeMaps(config.EnvStageProperty(env, stage), c.Properties) @@ -71,12 +65,11 @@ func (c *Cluster) Render(config *ProjectConfig, env, stage string) error { return fmt.Errorf("failed to load base templates: %w", err) } + addonProperties := c.AddonProperties(config, env, stage) addons := map[string]template.AddonData{} - for k, v := range c.Addons { - if !v.Enabled { - continue - } + for k, v := range addonProperties { addons[k] = template.AddonData{ + Enabled: v.Enabled, Annotations: config.ParsedAddons[k].Annotations, Properties: v.Properties, } @@ -103,10 +96,11 @@ func (c *Cluster) Render(config *ProjectConfig, env, stage string) error { return fmt.Errorf("failed to load addon %s templates: %w, value: %+v", addonName, err, config.ParsedAddons[addonName]) } err = atc.Render(config.BasePath, template.AddonTemplateData{ - Environment: env, - Stage: stage, - Cluster: c.Name, - Properties: addonValue.Properties, + Environment: env, + Stage: stage, + Cluster: c.Name, + ClusterProperties: properties, + Properties: addonValue.Properties, }) if err != nil { return fmt.Errorf("failed to render addon: %s, Error: %w", addonName, err) @@ -115,20 +109,6 @@ func (c *Cluster) Render(config *ProjectConfig, env, stage string) error { return nil } -func (c *Cluster) AllRequiredPropertiesSet(config *ProjectConfig) error { - for addonName, addon := range c.Addons { - if !addon.Enabled { - // skip disabled addons - continue - } - err := addon.AllRequiredPropertiesSet(config, addonName) - if err != nil { - return fmt.Errorf("failed to validate properties for addon %s: %w", addonName, err) - } - } - return nil -} - // SetDefaultAddons sets the default addons for the cluster func (c *Cluster) SetDefaultAddons(config *ProjectConfig) { for addonName, addon := range config.Addons { @@ -154,3 +134,28 @@ func (c *Cluster) SetDefaultAddons(config *ProjectConfig) { c.Addons[addonName] = cAddon } } + +// AddonProperties returns the addon properties for the cluster merged with the environment and stage properties +func (c *Cluster) AddonProperties(config *ProjectConfig, env, stg string) map[string]*ClusterAddon { + properties := c.Addons + for addonName, addon := range c.Addons { + if !addon.Enabled { + // addon was disabled on the cluster level, we skip it + continue + } + envAddonProps := map[string]any{} + if env := config.GetEnvironment(env).GetAddon(addonName); env != nil { + envAddonProps = env.Properties + } + stageAddonProps := map[string]any{} + if stg := config.GetStage(env, stg).GetAddon(addonName); stg != nil { + stageAddonProps = stg.Properties + } + addonProps := map[string]any{} + for key, property := range config.ParsedAddons[addonName].Properties { + addonProps[key] = property.Default + } + properties[addonName].Properties = utils.MergeMaps(addonProps, envAddonProps, stageAddonProps, c.GetAddon(addonName).Properties) + } + return properties +} diff --git a/internal/project/cluster_test.go b/internal/project/cluster_test.go index 532af9e..c510288 100644 --- a/internal/project/cluster_test.go +++ b/internal/project/cluster_test.go @@ -1,351 +1,13 @@ package project import ( + "reflect" "testing" "github.com/google/go-cmp/cmp" "github.com/leonsteinhaeuser/openshift-gitops-cli/internal/template" ) -func TestClusterAddon_AllRequiredPropertiesSet(t *testing.T) { - type fields struct { - Enabled bool - Properties map[string]any - } - type args struct { - config *ProjectConfig - addonName string - } - tests := []struct { - name string - fields fields - args args - wantErr bool - }{ - { - name: "no properties defined in addon properties", - fields: fields{ - Properties: map[string]any{}, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeBool, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: true, - }, - { - name: "one property set", - fields: fields{ - Properties: map[string]any{ - "property1": true, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeBool, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: false, - }, - { - name: "one property set string, expect bool", - fields: fields{ - Properties: map[string]any{ - "property1": "invalid", - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeBool, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: true, - }, - { - name: "one property set int, expect bool", - fields: fields{ - Properties: map[string]any{ - "property1": 10, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeBool, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: true, - }, - { - name: "one property set bool, expect string", - fields: fields{ - Properties: map[string]any{ - "property1": true, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeString, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: true, - }, - { - name: "one property set int, expect string", - fields: fields{ - Properties: map[string]any{ - "property1": 10, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeString, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: true, - }, - { - name: "one property set string, expect string", - fields: fields{ - Properties: map[string]any{ - "property1": "value", - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeString, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: false, - }, - { - name: "one property set int, expect int", - fields: fields{ - Properties: map[string]any{ - "property1": 10, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeInt, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: false, - }, - { - name: "one property set string, expect int", - fields: fields{ - Properties: map[string]any{ - "property1": "value", - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeInt, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: true, - }, - { - name: "one property set int, expect int", - fields: fields{ - Properties: map[string]any{ - "property1": 10, - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeInt, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: false, - }, - { - name: "one property set int, expect int, invalid value", - fields: fields{ - Properties: map[string]any{ - "property1": "invalid", - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeInt, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: true, - }, - { - name: "one property set int, expect int, invalid value", - fields: fields{ - Properties: map[string]any{ - "property1": "invalid", - }, - }, - args: args{ - config: &ProjectConfig{ - ParsedAddons: map[string]template.TemplateManifest{ - "addon1": { - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeInt, - Description: "property1", - }, - }, - }, - }, - }, - addonName: "addon1", - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ca := &ClusterAddon{ - Enabled: tt.fields.Enabled, - Properties: tt.fields.Properties, - } - if err := ca.AllRequiredPropertiesSet(tt.args.config, tt.args.addonName); (err != nil) != tt.wantErr { - t.Errorf("ClusterAddon.AllRequiredPropertiesSet() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - func TestCluster_IsAddonEnabled(t *testing.T) { type fields struct { Name string @@ -702,18 +364,212 @@ func TestCluster_DisableAddon(t *testing.T) { Addons: tt.fields.Addons, Properties: tt.fields.Properties, } - c.DisableAddon(tt.args.addon) - - diff := cmp.Diff(tt.want, *c) - if diff != "" { - t.Errorf("Cluster.DisableAddon() mismatch (-want +got):\n%s", diff) - return + c.DisableAddon(tt.args.addon) + + diff := cmp.Diff(tt.want, *c) + if diff != "" { + t.Errorf("Cluster.DisableAddon() mismatch (-want +got):\n%s", diff) + return + } + }) + } +} + +func TestCluster_GetAddons(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + tests := []struct { + name string + fields fields + want ClusterAddons + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + want: map[string]*ClusterAddon{}, + }, + { + name: "one addon", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + { + name: "two addons", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Cluster{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + if got := c.GetAddons(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Cluster.GetAddons() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCluster_GetAddon(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + name string + } + tests := []struct { + name string + fields fields + args args + want *ClusterAddon + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + name: "addon1", + }, + want: nil, + }, + { + name: "one addon", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + args: args{ + name: "addon1", + }, + want: &ClusterAddon{ + Enabled: true, + }, + }, + { + name: "two addons", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + name: "addon1", + }, + want: &ClusterAddon{ + Enabled: true, + }, + }, + { + name: "two addons no found", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + name: "addon5", + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Cluster{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + if got := c.GetAddon(tt.args.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Cluster.GetAddon() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestCluster_Render(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + config *ProjectConfig + env string + stage string + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Cluster{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + if err := c.Render(tt.args.config, tt.args.env, tt.args.stage); (err != nil) != tt.wantErr { + t.Errorf("Cluster.Render() error = %v, wantErr %v", err, tt.wantErr) } }) } } -func TestCluster_AllRequiredPropertiesSet(t *testing.T) { +func TestCluster_SetDefaultAddons(t *testing.T) { type fields struct { Name string Addons map[string]*ClusterAddon @@ -723,253 +579,433 @@ func TestCluster_AllRequiredPropertiesSet(t *testing.T) { config *ProjectConfig } tests := []struct { - name string - fields fields - args args - wantErr bool + name string + fields fields + args args + wantAddons map[string]*ClusterAddon }{ { - name: "one addon, no properties in cluster", + name: "one addon with bool property false", fields: fields{ - Addons: map[string]*ClusterAddon{ - "addon1": { - Enabled: true, - Properties: map[string]any{}, - }, - }, + Addons: map[string]*ClusterAddon{}, }, args: args{ config: &ProjectConfig{ + Addons: map[string]Addon{ + "addon1": { + Group: "group1", + DefaultEnabled: true, + }, + }, ParsedAddons: map[string]template.TemplateManifest{ "addon1": { - BasePath: "examples/addons/addon1", - Name: "addon1", - Description: "addon1", - Group: "group1", Properties: map[string]template.Property{ "property1": { Required: true, - Default: nil, + Default: false, Type: template.PropertyTypeBool, Description: "property1", }, }, - Annotations: map[string]string{}, - Files: []string{}, }, }, + }, + }, + wantAddons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": false}, + }, + }, + }, + { + name: "one addon with bool property nil", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + config: &ProjectConfig{ Addons: map[string]Addon{ "addon1": { - Name: "addon1", Group: "group1", DefaultEnabled: true, - Path: "examples/addons/addon1", }, }, + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeBool, + Description: "property1", + }, + }, + }, + }, + }, + }, + wantAddons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": nil}, }, }, - wantErr: true, }, { - name: "one addon, with properties in cluster but wrong type", + name: "one addon with int property 10", fields: fields{ - Addons: map[string]*ClusterAddon{ - "addon1": { - Enabled: true, - Properties: map[string]any{ - "property1": "invalid", - }, - }, - }, + Addons: map[string]*ClusterAddon{}, }, args: args{ config: &ProjectConfig{ + Addons: map[string]Addon{ + "addon1": { + Group: "group1", + DefaultEnabled: true, + }, + }, ParsedAddons: map[string]template.TemplateManifest{ "addon1": { - BasePath: "examples/addons/addon1", - Name: "addon1", - Description: "addon1", - Group: "group1", Properties: map[string]template.Property{ "property1": { Required: true, - Default: nil, - Type: template.PropertyTypeBool, + Default: 10, + Type: template.PropertyTypeInt, Description: "property1", }, }, - Annotations: map[string]string{}, - Files: []string{}, }, }, + }, + }, + wantAddons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": 10}, + }, + }, + }, + { + name: "one addon with int property nil", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + config: &ProjectConfig{ Addons: map[string]Addon{ "addon1": { - Name: "addon1", Group: "group1", DefaultEnabled: true, - Path: "examples/addons/addon1", }, }, + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeInt, + Description: "property1", + }, + }, + }, + }, + }, + }, + wantAddons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": nil}, }, }, - wantErr: true, }, { - name: "one addon disabled, with properties in cluster but wrong type", + name: "one addon with string property 'Hello World'", fields: fields{ - Addons: map[string]*ClusterAddon{ - "addon1": { - Enabled: false, - Properties: map[string]any{ - "property1": "invalid", - }, - }, - }, + Addons: map[string]*ClusterAddon{}, }, args: args{ config: &ProjectConfig{ + Addons: map[string]Addon{ + "addon1": { + Group: "group1", + DefaultEnabled: true, + }, + }, ParsedAddons: map[string]template.TemplateManifest{ "addon1": { - BasePath: "examples/addons/addon1", - Name: "addon1", - Description: "addon1", - Group: "group1", Properties: map[string]template.Property{ "property1": { Required: true, - Default: nil, - Type: template.PropertyTypeBool, + Default: "Hello World", + Type: template.PropertyTypeString, Description: "property1", }, }, - Annotations: map[string]string{}, - Files: []string{}, }, }, + }, + }, + wantAddons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": "Hello World"}, + }, + }, + }, + { + name: "one addon with string property 'nil'", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + config: &ProjectConfig{ Addons: map[string]Addon{ "addon1": { - Name: "addon1", Group: "group1", DefaultEnabled: true, - Path: "examples/addons/addon1", }, }, + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: nil, + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + }, + }, + wantAddons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": nil}, }, }, - wantErr: false, }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Cluster{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + c.SetDefaultAddons(tt.args.config) + + diff := cmp.Diff(tt.wantAddons, c.Addons) + if diff != "" { + t.Errorf("Cluster.SetDefaultAddons() mismatch (-want +got):\n%s", diff) + return + } + }) + } +} + +func TestCluster_AddonProperties(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + config *ProjectConfig + env string + stg string + } + tests := []struct { + name string + fields fields + args args + want map[string]*ClusterAddon + }{ { - name: "one addon, with properties in cluster", + name: "one addon, cluster has highest priority", fields: fields{ + Name: "cluster1", Addons: map[string]*ClusterAddon{ "addon1": { Enabled: true, Properties: map[string]any{ - "property1": true, + "property1": "value1", }, }, }, }, args: args{ config: &ProjectConfig{ + Addons: map[string]Addon{ + "addon1": { + Group: "group1", + DefaultEnabled: true, + }, + }, ParsedAddons: map[string]template.TemplateManifest{ "addon1": { - BasePath: "examples/addons/addon1", - Name: "addon1", - Description: "addon1", - Group: "group1", Properties: map[string]template.Property{ "property1": { Required: true, - Default: nil, - Type: template.PropertyTypeBool, + Default: "Hello World", + Type: template.PropertyTypeString, Description: "property1", }, }, - Annotations: map[string]string{}, - Files: []string{}, }, }, - Addons: map[string]Addon{ - "addon1": { - Name: "addon1", - Group: "group1", - DefaultEnabled: true, + Environments: map[string]*Environment{ + "env1": { + Stages: map[string]*Stage{ + "stage1": { + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{}, + }, + }, + }, + }, }, }, }, + env: "env1", + stg: "stage1", + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": "value1"}, + }, }, - wantErr: false, }, { - name: "two addons, with properties in cluster", + name: "one addon, stage has highest priority", fields: fields{ + Name: "cluster1", Addons: map[string]*ClusterAddon{ "addon1": { - Enabled: true, - Properties: map[string]any{ - "property1": true, - }, - }, - "addon2": { - Enabled: true, - Properties: map[string]any{ - "property1": true, - }, + Enabled: true, + Properties: map[string]any{}, }, }, }, args: args{ config: &ProjectConfig{ + Addons: map[string]Addon{ + "addon1": { + Group: "group1", + DefaultEnabled: true, + }, + }, ParsedAddons: map[string]template.TemplateManifest{ "addon1": { - BasePath: "examples/addons/addon1", - Name: "addon1", - Description: "addon1", - Group: "group1", Properties: map[string]template.Property{ "property1": { Required: true, - Default: nil, - Type: template.PropertyTypeBool, + Default: "Hello World", + Type: template.PropertyTypeString, Description: "property1", }, }, - Annotations: map[string]string{}, - Files: []string{}, }, - "addon2": { - BasePath: "examples/addons/addon2", - Name: "addon2", - Description: "addon2", - Group: "group2", - Properties: map[string]template.Property{ - "property1": { - Required: true, - Default: nil, - Type: template.PropertyTypeBool, - Description: "property1", + }, + Environments: map[string]*Environment{ + "env1": { + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "value1", + }, + }, + }, + Stages: map[string]*Stage{ + "stage1": { + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "value1", + }, + }, + }, }, }, - Annotations: map[string]string{}, - Files: []string{}, }, }, + }, + env: "env1", + stg: "stage1", + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": "value1"}, + }, + }, + }, + { + name: "one addon, env has highest priority", + fields: fields{ + Name: "cluster1", + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{}, + }, + }, + }, + args: args{ + config: &ProjectConfig{ Addons: map[string]Addon{ "addon1": { - Name: "addon1", Group: "group1", DefaultEnabled: true, }, - "addon2": { - Name: "addon2", - Group: "group2", - DefaultEnabled: true, + }, + ParsedAddons: map[string]template.TemplateManifest{ + "addon1": { + Properties: map[string]template.Property{ + "property1": { + Required: true, + Default: "Hello World", + Type: template.PropertyTypeString, + Description: "property1", + }, + }, + }, + }, + Environments: map[string]*Environment{ + "env1": { + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{ + "property1": "value1", + }, + }, + }, + Stages: map[string]*Stage{ + "stage1": { + Addons: map[string]*ClusterAddon{}, + }, + }, }, }, }, + env: "env1", + stg: "stage1", + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{"property1": "value1"}, + }, }, - wantErr: false, }, } for _, tt := range tests { @@ -979,8 +1015,11 @@ func TestCluster_AllRequiredPropertiesSet(t *testing.T) { Addons: tt.fields.Addons, Properties: tt.fields.Properties, } - if err := c.AllRequiredPropertiesSet(tt.args.config); (err != nil) != tt.wantErr { - t.Errorf("Cluster.AllRequiredPropertiesSet() error = %v, wantErr %v", err, tt.wantErr) + got := c.AddonProperties(tt.args.config, tt.args.env, tt.args.stg) + diff := cmp.Diff(tt.want, got) + if diff != "" { + t.Errorf("Cluster.AddonProperties() mismatch (-want +got):\n%s", diff) + return } }) } diff --git a/internal/project/environment.go b/internal/project/environment.go index 3593d87..e15f5e0 100644 --- a/internal/project/environment.go +++ b/internal/project/environment.go @@ -1,8 +1,63 @@ package project +var ( + _ AddonHandler = &Environment{} +) + type Environment struct { - Name string `json:"-"` - Properties map[string]string `json:"properties"` - Actions Actions `json:"actions"` - Stages map[string]*Stage `json:"stages"` + Name string `json:"-"` + Properties map[string]string `json:"properties"` + Actions Actions `json:"actions"` + Stages map[string]*Stage `json:"stages"` + Addons map[string]*ClusterAddon `json:"addons"` +} + +// IsAddonEnabled checks if the addon is enabled for the stage +func (e Environment) IsAddonEnabled(addon string) bool { + _, ok := e.Addons[addon] + if !ok { + return false + } + return e.Addons[addon].Enabled +} + +// EnableAddon enables the addon for the stage by setting the enabled flag to true +func (e *Environment) EnableAddon(addon string) { + if e.Addons[addon] == nil { + e.Addons[addon] = &ClusterAddon{ + Enabled: true, + } + return + } + e.Addons[addon].Enabled = true +} + +// DisableAddon disables the addon for the stage by setting the enabled flag to false +func (e *Environment) DisableAddon(addon string) { + if _, ok := e.Addons[addon]; !ok { + // already disabled or not found + return + } + e.Addons[addon].Enabled = false +} + +// GetAddons returns the environment addons +func (e *Environment) GetAddons() ClusterAddons { + return e.Addons +} + +// GetAddon returns the addon by name +func (e *Environment) GetAddon(name string) *ClusterAddon { + return e.Addons[name] +} + +// HasStage checks if a stage exists in the environment +func (e *Environment) HasStage(name string) bool { + _, ok := e.Stages[name] + return ok +} + +// GetStage returns the stage by name +func (e *Environment) GetStage(name string) *Stage { + return e.Stages[name] } diff --git a/internal/project/environment_test.go b/internal/project/environment_test.go new file mode 100644 index 0000000..d8cbaa5 --- /dev/null +++ b/internal/project/environment_test.go @@ -0,0 +1,682 @@ +package project + +import ( + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestEnvironment_IsAddonEnabled(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + addon string + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "one addon, enabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: true, + }, + { + name: "one addon, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "no addon, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "two addons, enabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: true, + }, + { + name: "two addons, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "two addons, one enabled, one disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + "addon2": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "two addons, one enabled, one disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := Environment{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + if got := e.IsAddonEnabled(tt.args.addon); got != tt.want { + t.Errorf("Environment.IsAddonEnabled() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEnvironment_EnableAddon(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + addon string + } + tests := []struct { + name string + fields fields + args args + want Environment + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + addon: "addon1", + }, + want: Environment{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + }, + { + name: "one addon, enabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Environment{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + }, + { + name: "one addon, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Environment{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + }, + { + name: "two addons, one enabled, one disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Environment{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + }, + { + name: "two addons, one enabled, one disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + "addon2": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Environment{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: true, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Environment{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + e.EnableAddon(tt.args.addon) + + diff := cmp.Diff(tt.want, *e) + if diff != "" { + t.Errorf("Environment.EnableAddon() mismatch (-want +got):\n%s", diff) + return + } + }) + } +} + +func TestEnvironment_DisableAddon(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + addon string + } + tests := []struct { + name string + fields fields + args args + want Environment + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + addon: "addon1", + }, + want: Environment{ + Addons: map[string]*ClusterAddon{}, + }, + }, + { + name: "one addon, enabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{}, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Environment{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + Properties: map[string]any{}, + }, + }, + }, + }, + { + name: "one addon, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Environment{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + Properties: nil, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Environment{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + e.DisableAddon(tt.args.addon) + + diff := cmp.Diff(tt.want, *e) + if diff != "" { + t.Errorf("Environment.DisableAddon() mismatch (-want +got):\n%s", diff) + return + } + }) + } +} + +func TestEnvironment_GetAddons(t *testing.T) { + type fields struct { + Name string + Properties map[string]string + Actions Actions + Stages map[string]*Stage + Addons map[string]*ClusterAddon + } + tests := []struct { + name string + fields fields + want ClusterAddons + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + want: map[string]*ClusterAddon{}, + }, + { + name: "one addon", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + { + name: "two addons", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Environment{ + Name: tt.fields.Name, + Properties: tt.fields.Properties, + Actions: tt.fields.Actions, + Stages: tt.fields.Stages, + Addons: tt.fields.Addons, + } + if got := e.GetAddons(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Environment.GetAddons() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEnvironment_GetAddon(t *testing.T) { + type fields struct { + Name string + Properties map[string]string + Actions Actions + Stages map[string]*Stage + Addons map[string]*ClusterAddon + } + type args struct { + name string + } + tests := []struct { + name string + fields fields + args args + want *ClusterAddon + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + name: "addon1", + }, + want: nil, + }, + { + name: "one addon", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + args: args{ + name: "addon1", + }, + want: &ClusterAddon{ + Enabled: true, + }, + }, + { + name: "two addons", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + name: "addon1", + }, + want: &ClusterAddon{ + Enabled: true, + }, + }, + { + name: "two addons no found", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + name: "addon5", + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Environment{ + Name: tt.fields.Name, + Properties: tt.fields.Properties, + Actions: tt.fields.Actions, + Stages: tt.fields.Stages, + Addons: tt.fields.Addons, + } + if got := e.GetAddon(tt.args.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Environment.GetAddon() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEnvironment_HasStage(t *testing.T) { + type fields struct { + Name string + Properties map[string]string + Actions Actions + Stages map[string]*Stage + Addons map[string]*ClusterAddon + } + type args struct { + name string + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "no stages", + fields: fields{ + Stages: map[string]*Stage{}, + }, + args: args{ + name: "stage1", + }, + want: false, + }, + { + name: "one stage", + fields: fields{ + Stages: map[string]*Stage{ + "stage1": {}, + }, + }, + args: args{ + name: "stage1", + }, + want: true, + }, + { + name: "two stages and found", + fields: fields{ + Stages: map[string]*Stage{ + "stage1": {}, + "stage2": {}, + }, + }, + args: args{ + name: "stage1", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Environment{ + Name: tt.fields.Name, + Properties: tt.fields.Properties, + Actions: tt.fields.Actions, + Stages: tt.fields.Stages, + Addons: tt.fields.Addons, + } + if got := e.HasStage(tt.args.name); got != tt.want { + t.Errorf("Environment.HasStage() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEnvironment_GetStage(t *testing.T) { + type fields struct { + Name string + Properties map[string]string + Actions Actions + Stages map[string]*Stage + Addons map[string]*ClusterAddon + } + type args struct { + name string + } + tests := []struct { + name string + fields fields + args args + want *Stage + }{ + { + name: "no stages", + fields: fields{ + Stages: map[string]*Stage{}, + }, + args: args{ + name: "stage1", + }, + want: nil, + }, + { + name: "one stage", + fields: fields{ + Stages: map[string]*Stage{ + "stage1": {}, + }, + }, + args: args{ + name: "stage1", + }, + want: &Stage{}, + }, + { + name: "two stages and found", + fields: fields{ + Stages: map[string]*Stage{ + "stage1": {}, + "stage2": {}, + }, + }, + args: args{ + name: "stage1", + }, + want: &Stage{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := &Environment{ + Name: tt.fields.Name, + Properties: tt.fields.Properties, + Actions: tt.fields.Actions, + Stages: tt.fields.Stages, + Addons: tt.fields.Addons, + } + if got := e.GetStage(tt.args.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Environment.GetStage() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/project/stage.go b/internal/project/stage.go index d3cff46..c218922 100644 --- a/internal/project/stage.go +++ b/internal/project/stage.go @@ -1,8 +1,57 @@ package project +var ( + _ AddonHandler = &Stage{} +) + type Stage struct { - Name string `json:"-"` - Properties map[string]string `json:"properties"` - Actions Actions `json:"actions"` - Clusters map[string]*Cluster `json:"clusters"` + Name string `json:"-"` + Properties map[string]string `json:"properties"` + Actions Actions `json:"actions"` + Clusters map[string]*Cluster `json:"clusters"` + Addons map[string]*ClusterAddon `json:"addons"` +} + +// IsAddonEnabled checks if the addon is enabled for the stage +func (s Stage) IsAddonEnabled(addon string) bool { + _, ok := s.Addons[addon] + if !ok { + return false + } + return s.Addons[addon].Enabled +} + +// EnableAddon enables the addon for the stage by setting the enabled flag to true +func (s *Stage) EnableAddon(addon string) { + if s.Addons[addon] == nil { + s.Addons[addon] = &ClusterAddon{ + Enabled: true, + } + return + } + s.Addons[addon].Enabled = true +} + +// DisableAddon disables the addon for the stage by setting the enabled flag to false +func (s *Stage) DisableAddon(addon string) { + if _, ok := s.Addons[addon]; !ok { + // already disabled or not found + return + } + s.Addons[addon].Enabled = false +} + +// GetAddons returns the environment addons +func (s *Stage) GetAddons() ClusterAddons { + return s.Addons +} + +// GetAddon returns the addon by name +func (s *Stage) GetAddon(name string) *ClusterAddon { + return s.Addons[name] +} + +// GetCluster returns the cluster by name +func (s *Stage) GetCluster(name string) *Cluster { + return s.Clusters[name] } diff --git a/internal/project/stage_test.go b/internal/project/stage_test.go new file mode 100644 index 0000000..d0a276b --- /dev/null +++ b/internal/project/stage_test.go @@ -0,0 +1,640 @@ +package project + +import ( + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestStage_IsAddonEnabled(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + addon string + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "one addon, enabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: true, + }, + { + name: "one addon, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "no addon, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "two addons, enabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: true, + }, + { + name: "two addons, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "two addons, one enabled, one disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + "addon2": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: false, + }, + { + name: "two addons, one enabled, one disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := Stage{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + if got := s.IsAddonEnabled(tt.args.addon); got != tt.want { + t.Errorf("Stage.IsAddonEnabled() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStage_EnableAddon(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + addon string + } + tests := []struct { + name string + fields fields + args args + want Stage + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + addon: "addon1", + }, + want: Stage{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + }, + { + name: "one addon, enabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Stage{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + }, + { + name: "one addon, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Stage{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + }, + { + name: "two addons, one enabled, one disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Stage{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + }, + { + name: "two addons, one enabled, one disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + "addon2": { + Enabled: true, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Stage{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: true, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Stage{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + s.EnableAddon(tt.args.addon) + + diff := cmp.Diff(tt.want, *s) + if diff != "" { + t.Errorf("Stage.EnableAddon() mismatch (-want +got):\n%s", diff) + return + } + }) + } +} + +func TestStage_DisableAddon(t *testing.T) { + type fields struct { + Name string + Addons map[string]*ClusterAddon + Properties map[string]string + } + type args struct { + addon string + } + tests := []struct { + name string + fields fields + args args + want Stage + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + addon: "addon1", + }, + want: Stage{ + Addons: map[string]*ClusterAddon{}, + }, + }, + { + name: "one addon, enabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + Properties: map[string]any{}, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Stage{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + Properties: map[string]any{}, + }, + }, + }, + }, + { + name: "one addon, disabled", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + }, + }, + }, + args: args{ + addon: "addon1", + }, + want: Stage{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: false, + Properties: nil, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Stage{ + Name: tt.fields.Name, + Addons: tt.fields.Addons, + Properties: tt.fields.Properties, + } + s.DisableAddon(tt.args.addon) + + diff := cmp.Diff(tt.want, *s) + if diff != "" { + t.Errorf("Stage.DisableAddon() mismatch (-want +got):\n%s", diff) + return + } + }) + } +} + +func TestStage_GetAddons(t *testing.T) { + type fields struct { + Name string + Properties map[string]string + Actions Actions + Clusters map[string]*Cluster + Addons map[string]*ClusterAddon + } + tests := []struct { + name string + fields fields + want ClusterAddons + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + want: map[string]*ClusterAddon{}, + }, + { + name: "one addon", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + { + name: "two addons", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + want: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Stage{ + Name: tt.fields.Name, + Properties: tt.fields.Properties, + Actions: tt.fields.Actions, + Clusters: tt.fields.Clusters, + Addons: tt.fields.Addons, + } + if got := s.GetAddons(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Stage.GetAddons() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStage_GetAddon(t *testing.T) { + type fields struct { + Name string + Properties map[string]string + Actions Actions + Clusters map[string]*Cluster + Addons map[string]*ClusterAddon + } + type args struct { + name string + } + tests := []struct { + name string + fields fields + args args + want *ClusterAddon + }{ + { + name: "no addons", + fields: fields{ + Addons: map[string]*ClusterAddon{}, + }, + args: args{ + name: "addon1", + }, + want: nil, + }, + { + name: "one addon", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + }, + }, + args: args{ + name: "addon1", + }, + want: &ClusterAddon{ + Enabled: true, + }, + }, + { + name: "two addons", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + name: "addon1", + }, + want: &ClusterAddon{ + Enabled: true, + }, + }, + { + name: "two addons no found", + fields: fields{ + Addons: map[string]*ClusterAddon{ + "addon1": { + Enabled: true, + }, + "addon2": { + Enabled: false, + }, + }, + }, + args: args{ + name: "addon5", + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Stage{ + Name: tt.fields.Name, + Properties: tt.fields.Properties, + Actions: tt.fields.Actions, + Clusters: tt.fields.Clusters, + Addons: tt.fields.Addons, + } + if got := s.GetAddon(tt.args.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Stage.GetAddon() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStage_GetCluster(t *testing.T) { + type fields struct { + Name string + Properties map[string]string + Actions Actions + Clusters map[string]*Cluster + Addons map[string]*ClusterAddon + } + type args struct { + name string + } + tests := []struct { + name string + fields fields + args args + want *Cluster + }{ + { + name: "no clusters", + fields: fields{ + Clusters: map[string]*Cluster{}, + }, + args: args{ + name: "cluster1", + }, + want: nil, + }, + { + name: "one cluster", + fields: fields{ + Clusters: map[string]*Cluster{ + "cluster1": { + Name: "cluster1", + }, + }, + }, + args: args{ + name: "cluster1", + }, + want: &Cluster{ + Name: "cluster1", + }, + }, + { + name: "two clusters", + fields: fields{ + Clusters: map[string]*Cluster{ + "cluster1": { + Name: "cluster1", + }, + "cluster2": { + Name: "cluster2", + }, + }, + }, + args: args{ + name: "cluster1", + }, + want: &Cluster{ + Name: "cluster1", + }, + }, + { + name: "two clusters no found", + fields: fields{ + Clusters: map[string]*Cluster{ + "cluster1": { + Name: "cluster1", + }, + "cluster2": { + Name: "cluster2", + }, + }, + }, + args: args{ + name: "cluster5", + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &Stage{ + Name: tt.fields.Name, + Properties: tt.fields.Properties, + Actions: tt.fields.Actions, + Clusters: tt.fields.Clusters, + Addons: tt.fields.Addons, + } + if got := s.GetCluster(tt.args.name); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Stage.GetCluster() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/project/type.go b/internal/project/type.go index 22c4dd4..c643016 100644 --- a/internal/project/type.go +++ b/internal/project/type.go @@ -22,29 +22,25 @@ type ProjectConfig struct { // HasCluster checks if a cluster exists in the given environment and stage func (p ProjectConfig) HasCluster(env, stage, cluster string) bool { - _, ok := p.Environments[env].Stages[stage].Clusters[cluster] + _, ok := p.GetStage(env, stage).Clusters[cluster] return ok } -func (p ProjectConfig) Cluster(env, stage, cluster string) *Cluster { - return p.Environments[env].Stages[stage].Clusters[cluster] -} - // SetCluster sets the cluster for the given environment and stage func (p *ProjectConfig) SetCluster(env, stage string, cluster *Cluster) { - if p.Environments[env].Stages[stage].Clusters == nil { - p.Environments[env].Stages[stage].Clusters = map[string]*Cluster{} + if p.GetStage(env, stage).Clusters == nil { + p.GetStage(env, stage).Clusters = map[string]*Cluster{} } - p.Environments[env].Stages[stage].Clusters[cluster.Name] = cluster + p.GetStage(env, stage).Clusters[cluster.Name] = cluster } func (p *ProjectConfig) DeleteCluster(env, stage, cluster string) { - delete(p.Environments[env].Stages[stage].Clusters, cluster) + delete(p.GetStage(env, stage).Clusters, cluster) } // EnvStageProperty merges the properties of the environment and stage and returns them as a map func (pc *ProjectConfig) EnvStageProperty(environment, stage string) map[string]string { - return utils.MergeMaps(pc.Environments[environment].Properties, pc.Environments[environment].Stages[stage].Properties) + return utils.MergeMaps(pc.GetEnvironment(environment).Properties, pc.GetStage(environment, stage).Properties) } // AddonGroups returns a list of addon groups that have been defined in the addons @@ -58,3 +54,23 @@ func (p ProjectConfig) AddonGroups() []string { } return utils.MapKeysToList(groups) } + +// HasEnvironment checks if an environment exists in the project +func (p ProjectConfig) HasEnvironment(name string) bool { + _, ok := p.Environments[name] + return ok +} + +func (p *ProjectConfig) GetEnvironment(name string) *Environment { + p.Environments[name].Name = name + return p.Environments[name] +} + +func (p *ProjectConfig) GetStage(env, stage string) *Stage { + p.GetEnvironment(env).GetStage(stage).Name = stage + return p.GetEnvironment(env).GetStage(stage) +} + +func (p *ProjectConfig) GetCluster(env, stage, cluster string) *Cluster { + return p.GetStage(env, stage).GetCluster(cluster) +} diff --git a/internal/project/type_test.go b/internal/project/type_test.go index 1e77d5f..7816b1d 100644 --- a/internal/project/type_test.go +++ b/internal/project/type_test.go @@ -88,7 +88,7 @@ func TestProjectConfig_HasCluster(t *testing.T) { } } -func TestProjectConfig_Cluster(t *testing.T) { +func TestProjectConfig_GetCluster(t *testing.T) { type fields struct { BasePath string TemplateBasePath string @@ -172,10 +172,10 @@ func TestProjectConfig_Cluster(t *testing.T) { Environments: tt.fields.Environments, } - got := p.Cluster(tt.args.env, tt.args.stage, tt.args.cluster) + got := p.GetCluster(tt.args.env, tt.args.stage, tt.args.cluster) diff := cmp.Diff(got, tt.want) if diff != "" { - t.Errorf("ProjectConfig.Cluster() mismatch (-got +want):\n%s", diff) + t.Errorf("ProjectConfig.GetCluster() mismatch (-got +want):\n%s", diff) return } }) @@ -241,7 +241,7 @@ func TestProjectConfig_SetCluster(t *testing.T) { } p.SetCluster(tt.args.env, tt.args.stage, tt.args.cluster) - diff := cmp.Diff(p.Environments[tt.args.env].Stages[tt.args.stage].Clusters[tt.args.cluster.Name], tt.want) + diff := cmp.Diff(p.GetCluster(tt.args.env, tt.args.stage, tt.args.cluster.Name), tt.want) if diff != "" { t.Errorf("ProjectConfig.SetCluster() mismatch (-got +want):\n%s", diff) return @@ -327,7 +327,7 @@ func TestProjectConfig_DeleteCluster(t *testing.T) { } p.DeleteCluster(tt.args.env, tt.args.stage, tt.args.cluster) - diff := cmp.Diff(p.Environments[tt.args.env].Stages[tt.args.stage].Clusters, tt.want) + diff := cmp.Diff(p.GetStage(tt.args.env, tt.args.stage).Clusters, tt.want) if diff != "" { t.Errorf("ProjectConfig.DeleteCluster() mismatch (-got +want):\n%s", diff) return diff --git a/internal/template/addon.go b/internal/template/addon.go index 2d70a5d..0871b0d 100644 --- a/internal/template/addon.go +++ b/internal/template/addon.go @@ -74,10 +74,11 @@ func LoadTemplatesFromAddonManifest(source TemplateManifest) (*AddonTemplateCarr } type AddonTemplateData struct { - Environment string - Stage string - Cluster string - Properties map[string]any + Environment string + Stage string + Cluster string + ClusterProperties map[string]string + Properties map[string]any } func (a AddonTemplateCarrier) Render(basePath string, properties AddonTemplateData) error { diff --git a/internal/template/engine.go b/internal/template/engine.go index 8d39e17..3da3ddc 100644 --- a/internal/template/engine.go +++ b/internal/template/engine.go @@ -30,6 +30,7 @@ type TemplateData struct { } type AddonData struct { + Enabled bool Annotations map[string]string Properties map[string]any } diff --git a/internal/template/manifest.go b/internal/template/manifest.go index ac950e0..3f9fa2c 100644 --- a/internal/template/manifest.go +++ b/internal/template/manifest.go @@ -5,6 +5,8 @@ import ( "io/fs" "os" "path/filepath" + "reflect" + "strconv" "strings" "sigs.k8s.io/yaml" @@ -38,24 +40,45 @@ const ( // checkType validates the given value against the property type // If the value is valid, it will be returned, otherwise an error is returned func (p PropertyType) checkType(value any) (any, error) { - switch v := value.(type) { - case string: - if p != PropertyTypeString { - return nil, fmt.Errorf("expected type %s, got string", p) + if value == nil { + return nil, nil + } + + kind := reflect.TypeOf(value).Kind() + typeValue := reflect.ValueOf(value) + switch p { + case PropertyTypeString: + v, ok := value.(string) + if !ok { + return nil, fmt.Errorf("expected type %s, got %v", p, kind) } return v, nil - case bool: - if p != PropertyTypeBool { - return nil, fmt.Errorf("expected type %s, got bool", p) + case PropertyTypeBool: + if kind == reflect.String { + bl, err := strconv.ParseBool(typeValue.String()) + if err != nil { + return nil, err + } + return bl, nil } - return v, nil - case int: - if p != PropertyTypeInt { - return nil, fmt.Errorf("expected type %s, got int", p) + if kind == reflect.Bool { + return value, nil } - return v, nil + return nil, fmt.Errorf("expected type %s, got %v", p, kind) + case PropertyTypeInt: + if kind == reflect.String { + i, err := strconv.Atoi(typeValue.String()) + if err != nil { + return nil, err + } + return i, nil + } + if kind == reflect.Int { + return value, nil + } + return nil, fmt.Errorf("expected type %s, got %v", p, kind) default: - return nil, fmt.Errorf("unsupported type %T", v) + return nil, fmt.Errorf("expected type %s, got %v", p, reflect.TypeOf(value).Kind()) } } diff --git a/internal/template/manifest_test.go b/internal/template/manifest_test.go index f282357..1a00ffc 100644 --- a/internal/template/manifest_test.go +++ b/internal/template/manifest_test.go @@ -47,16 +47,16 @@ func TestPropertyType_checkType(t *testing.T) { args: args{ value: 42, }, - want: 42, + want: int(42), wantErr: false, }, { - name: "invalid string", + name: "number as string", p: PropertyTypeString, args: args{ value: 42, }, - want: nil, + want: "", wantErr: true, }, { @@ -86,6 +86,24 @@ func TestPropertyType_checkType(t *testing.T) { want: nil, wantErr: true, }, + { + name: "property is bool want bool as string", + p: PropertyTypeString, + args: args{ + value: true, + }, + want: "", + wantErr: true, + }, + { + name: "nil value", + p: PropertyTypeString, + args: args{ + value: nil, + }, + want: nil, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -94,8 +112,12 @@ func TestPropertyType_checkType(t *testing.T) { t.Errorf("PropertyType.checkType() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("PropertyType.checkType() = %v, want %v", got, tt.want) + if tt.wantErr { + return + } + diff := cmp.Diff(got, tt.want) + if diff != "" { + t.Errorf("PropertyType.checkType() mismatch (-want +got):\n%s", diff) } }) } diff --git a/internal/utils/color.go b/internal/utils/color.go new file mode 100644 index 0000000..7b0e203 --- /dev/null +++ b/internal/utils/color.go @@ -0,0 +1,27 @@ +package utils + +import "fmt" + +type Color string + +var ( + reset Color = "\033[0m" + Red Color = "\033[31m" + Green Color = "\033[32m" + Yellow Color = "\033[33m" + Blue Color = "\033[34m" + Magenta Color = "\033[35m" + Cyan Color = "\033[36m" + Gray Color = "\033[37m" + White Color = "\033[97m" +) + +// Wrap wraps the given string with the color +func (c Color) Wrap(s string) string { + return fmt.Sprintf("%s%s%s", c, s, reset) +} + +// Colorize colorizes the given string with the given color +func Colorize(s string, c Color) string { + return c.Wrap(s) +}