From dea6b06a8e5d3a401aa86f79bd0b0a9a3bcf2dc9 Mon Sep 17 00:00:00 2001 From: Dex Date: Fri, 17 Aug 2018 10:19:01 -0700 Subject: [PATCH] Ship init supports arbitrary upstreams What I Did ------------ Updated how we resolve releases so that in addition to helm charts, ship can pull an arbitrary directory of k8s manifests. We now clone the repo into .ship, and then inspect if there's a `Chart.yaml`. If there is, we moved the cloned repo path into `./chart`, and generate the values-kustomize lifecycle. If there's no `Chart.yaml`, we move it straight to `base`, and use a minimal lifecycle to drop the user straight into "kustomize". This is mostly a POC, and could probably use some tweaks on the UX side, esp around UI messages and their pacing/verbosity. It could probably also use some unit testing love, but this has good integration test coverage for now. How I Did it ------------ We've add a new workflow for resolving releases, to try and consolidate some of the various levels of abstraction in the `specs.Resolver` and in `kustomize.go`. In order: - fetch upstream from github or using `helm fetch` - determine application type based on the upstream we fetched. For now, type is one of `replicated.app`, `helm` or `k8s`, but i think we wanna add knative soon - Write out upstream and content sha to state and to ShipAppMetadata - If helm, add helm chart metadata to ShipAppMetadata as well - If helm+fetch, write helm fetch parameters like `--chart-version` to state.json - generate a release based on application type + discovered metadata There is a *lot* of stuff moving around in this PR with respect to "how do we get the release we hand off to daemon/lifecycle", it may be easier to read the flow of the new code, rather than trying to grok the diff. Changes in order of appearance: - refactor replicated.app spec fetching into specs/replicatedapp - refactor github client into specs/githubclient - new specs/apptype package for determining app type - move `init_chart` integration tests to `init`, which just allows for passing in arbitrary args like `--chart-version` etc. - rename `chartURL` to upstream everywhere - rename `HelmChartMetadata` to `ShipAppMetadata`, the this info is pretty generic and can apply to non-helm charts - Create a `.ship/tmp` for temporary workspace stuff, to be cleaned up when ship terminates. This lets components write to that dir, without having to worry about how/when to do a deferred cleanup. - Cobra commands do an os.MkdirAll on `.ship/tmp` before ship starts - Cobra commands do an os.RemoveAll on `.ship/tmp` as ship shuts down - On Shutdown, ship does an `os.RemoveAll` on `.ship/tmp` (this handles the case where cobra doesn't get to clean up, because ship did an os.Exit) - Rename some constants to be more generic for non-helm apps - use `BackupIfPresent` before removing any directories, move the function into a utililty package, add a terminal warning when backing up directories. - Rework specs.Resolver for the new workflow above - expand determineApplicationType to handle raw k8s case, as well as the old `helm fetch` case - add methods to migrate `state.ChartURL` to `state.Upstream` cleanly. - `helm` asset uses `.ship/tmp/.helm` as HELM_HOME Other fun stuff: - integration tests pretty-print json before diffing - `state.Manager` saves pretty-printed state json - Increase timeout from 60ms to 120ms on a flaky test - More fixes for weird tmpdir stuff on osx How to verify it ------------ Check out integration tests, but you can now do `ship init` for repo paths that have no helm chart, just k8s yaml. For example, you can try out [my fork of sysdig cloud's kubernetes installer](https://github.com/dexhorthy/sysdigcloud-kubernetes/tree/master/sysdigcloud): ``` ship init github.com/dexhorthy/sysdigcloud-kubernetes/sysdigcloud ``` Description for the Changelog ------------ Support fetching/kustomizing an arbitary directory of Kubernetes manifests with `ship init`. ![](https://upload.wikimedia.org/wikipedia/commons/thumb/a/a4/FrotamericaShipWreck.jpg/2560px-FrotamericaShipWreck.jpg) Special thanks to @jadengore who helped out on debugging and fixing a few things here, and whose commits got obliterated in what turned out to be a pretty messy rebase after the changes for `helm fetch`. --- .circleci/config.yml | 2 +- .gitignore | 2 + Makefile | 12 + deploy/e2e.yml | 18 - .../helm-fetch/expected/.ship/.helm/.gitkeep | 0 .../helm-nginx/expected/.ship/.helm/.gitkeep | 0 integration/base/integration_test.go | 4 +- .../{init_chart => init}/integration_test.go | 19 +- .../init/plain-k8s/expected/.ship/state.json | 7 + .../plain-k8s/expected/base/MAINTENANCE.md | 23 + .../init/plain-k8s/expected/base/README.md | 6 + .../expected/base/frontend-deployment.yaml | 33 ++ .../expected/base/frontend-service.yaml | 18 + .../expected/base/kustomization.yaml | 7 + .../base/redis-master-deployment.yaml | 27 + .../expected/base/redis-master-service.yaml | 16 + .../expected/base/redis-slave-deployment.yaml | 35 ++ .../expected/base/redis-slave-service.yaml | 15 + .../expected/overlays/ship/kustomization.yaml | 0 integration/init/plain-k8s/metadata.yaml | 3 + .../stable-mysql/expected/.ship/state.json | 2 +- .../expected/base/deployment.yaml | 0 .../expected/base/kustomization.yaml | 0 .../stable-mysql/expected/base/pvc.yaml | 0 .../stable-mysql/expected/base/secrets.yaml | 0 .../stable-mysql/expected/base/svc.yaml | 0 .../expected/base/tests/test-configmap.yaml | 0 .../expected/base/tests/test.yaml | 0 .../stable-mysql/expected/chart/.helmignore | 0 .../stable-mysql/expected/chart/Chart.yaml | 0 .../stable-mysql/expected/chart/README.md | 0 .../expected/chart/templates/NOTES.txt | 0 .../expected/chart/templates/_helpers.tpl | 0 .../configurationFiles-configmap.yaml | 0 .../expected/chart/templates/deployment.yaml | 0 .../initializationFiles-configmap.yaml | 0 .../expected/chart/templates/pvc.yaml | 0 .../expected/chart/templates/secrets.yaml | 0 .../expected/chart/templates/svc.yaml | 0 .../chart/templates/tests/test-configmap.yaml | 0 .../expected/chart/templates/tests/test.yaml | 0 .../stable-mysql/expected/chart/values.yaml | 0 .../expected/overlays/ship/kustomization.yaml | 2 + integration/init/stable-mysql/metadata.yaml | 5 + integration/init_app/integration_test.go | 10 +- .../expected/.ship/.helm/.gitkeep | 0 .../init_chart/stable-mysql/metadata.yaml | 3 - .../basic/expected/.ship/.helm/.gitkeep | 0 .../update/basic/expected/.ship/state.json | 2 +- .../update/basic/input/.ship/state.json | 2 +- .../expected/.ship/.helm/.gitkeep | 0 .../modify-chart/expected/.ship/state.json | 18 +- .../modify-chart/input/.ship/state.json | 2 +- integration/update/modify-chart/metadata.yaml | 1 + .../expected/.ship/.helm/.gitkeep | 0 .../values-static/expected/.ship/state.json | 18 +- .../values-static/input/.ship/state.json | 18 +- .../values-update/expected/.ship/state.json | 2 +- .../values-update/input/.ship/state.json | 14 +- pkg/api/spec.go | 30 +- pkg/cli/root.go | 8 + pkg/constants/filepaths.go | 61 ++- pkg/helm/shared.go | 2 +- pkg/lifecycle/daemon/headless/daemon.go | 2 +- .../routes_navcycle_completestep_test.go | 2 +- .../daemon/routes_navcycle_getstep.go | 2 +- pkg/lifecycle/daemon/routes_v1.go | 6 +- pkg/lifecycle/helmValues/helmValues.go | 4 +- pkg/lifecycle/kustomize/kustomizer_test.go | 22 +- pkg/lifecycle/render/backup.go | 30 +- .../render/dockerlayer/layer_test.go | 4 +- pkg/lifecycle/render/helm/commands.go | 23 +- pkg/lifecycle/render/helm/fetch.go | 32 +- pkg/lifecycle/render/helm/template.go | 9 +- pkg/lifecycle/render/helm/template_test.go | 7 +- pkg/lifecycle/render/render_test.go | 6 +- pkg/lifecycle/step.go | 2 +- pkg/logger/logger.go | 1 + pkg/patch/patcher.go | 2 +- pkg/ship/dig.go | 10 +- pkg/ship/kustomize.go | 101 +--- pkg/ship/ship.go | 51 +- pkg/specs/apptype/determine_type.go | 209 ++++++++ pkg/specs/chart.go | 481 +++++++----------- pkg/specs/chart_test.go | 186 +------ pkg/specs/constructor.go | 57 +++ pkg/specs/githubclient/client.go | 167 ++++++ pkg/specs/githubclient/client_test.go | 189 +++++++ pkg/specs/interface.go | 90 ++-- pkg/specs/interface_test.go | 201 ++++++++ pkg/specs/patch.go | 27 +- pkg/specs/{ => replicatedapp}/graphql.go | 2 +- pkg/specs/{ => replicatedapp}/resolver.go | 49 +- .../{ => replicatedapp}/resolver_test.go | 13 +- pkg/specs/{ => replicatedapp}/selector.go | 4 +- .../{ => replicatedapp}/selector_test.go | 4 +- pkg/state/manager.go | 34 +- pkg/state/manager_test.go | 12 +- pkg/state/models.go | 81 +-- pkg/test-mocks/apptype/determine_type_mock.go | 49 ++ pkg/test-mocks/helm/commands_mock.go | 12 + .../replicatedapp/resolve_replicated_app.go | 62 +++ pkg/test-mocks/state/manager_mock.go | 24 +- pkg/testing/matchers/matchers.go | 14 + pkg/util/filesystem.go | 25 + .../components/kustomize/HelmChartInfo.jsx | 4 +- .../components/kustomize/HelmValuesEditor.jsx | 2 +- .../shared/DetermineComponentForRoute.jsx | 4 +- web/src/components/shared/DetermineStep.jsx | 4 +- web/src/components/shared/NavBar.jsx | 8 +- web/src/components/shared/NavBar.spec.js | 10 +- .../containers/DetermineComponentForRoute.js | 2 +- web/src/containers/DetermineStep.js | 2 +- web/src/containers/HelmChartInfo.js | 2 +- web/src/containers/Navbar.js | 2 +- web/src/redux/data/kustomizeSettings/index.js | 4 +- .../redux/data/kustomizeSettings/reducer.js | 4 +- 117 files changed, 1830 insertions(+), 973 deletions(-) delete mode 100644 deploy/e2e.yml delete mode 100644 integration/base/helm-fetch/expected/.ship/.helm/.gitkeep delete mode 100644 integration/base/helm-nginx/expected/.ship/.helm/.gitkeep rename integration/{init_chart => init}/integration_test.go (85%) create mode 100644 integration/init/plain-k8s/expected/.ship/state.json create mode 100644 integration/init/plain-k8s/expected/base/MAINTENANCE.md create mode 100644 integration/init/plain-k8s/expected/base/README.md create mode 100644 integration/init/plain-k8s/expected/base/frontend-deployment.yaml create mode 100644 integration/init/plain-k8s/expected/base/frontend-service.yaml create mode 100644 integration/init/plain-k8s/expected/base/kustomization.yaml create mode 100644 integration/init/plain-k8s/expected/base/redis-master-deployment.yaml create mode 100644 integration/init/plain-k8s/expected/base/redis-master-service.yaml create mode 100644 integration/init/plain-k8s/expected/base/redis-slave-deployment.yaml create mode 100644 integration/init/plain-k8s/expected/base/redis-slave-service.yaml rename integration/{init_chart/stable-mysql => init/plain-k8s}/expected/overlays/ship/kustomization.yaml (100%) create mode 100644 integration/init/plain-k8s/metadata.yaml rename integration/{init_chart => init}/stable-mysql/expected/.ship/state.json (82%) rename integration/{init_chart => init}/stable-mysql/expected/base/deployment.yaml (100%) rename integration/{init_chart => init}/stable-mysql/expected/base/kustomization.yaml (100%) rename integration/{init_chart => init}/stable-mysql/expected/base/pvc.yaml (100%) rename integration/{init_chart => init}/stable-mysql/expected/base/secrets.yaml (100%) rename integration/{init_chart => init}/stable-mysql/expected/base/svc.yaml (100%) rename integration/{init_chart => init}/stable-mysql/expected/base/tests/test-configmap.yaml (100%) rename integration/{init_chart => init}/stable-mysql/expected/base/tests/test.yaml (100%) rename integration/{init_chart => init}/stable-mysql/expected/chart/.helmignore (100%) rename integration/{init_chart => init}/stable-mysql/expected/chart/Chart.yaml (100%) rename integration/{init_chart => init}/stable-mysql/expected/chart/README.md (100%) rename integration/{init_chart => init}/stable-mysql/expected/chart/templates/NOTES.txt (100%) rename integration/{init_chart => init}/stable-mysql/expected/chart/templates/_helpers.tpl (100%) rename integration/{init_chart => init}/stable-mysql/expected/chart/templates/configurationFiles-configmap.yaml (100%) rename integration/{init_chart => init}/stable-mysql/expected/chart/templates/deployment.yaml (100%) rename integration/{init_chart => init}/stable-mysql/expected/chart/templates/initializationFiles-configmap.yaml (100%) rename integration/{init_chart => init}/stable-mysql/expected/chart/templates/pvc.yaml (100%) rename integration/{init_chart => init}/stable-mysql/expected/chart/templates/secrets.yaml (100%) rename integration/{init_chart => init}/stable-mysql/expected/chart/templates/svc.yaml (100%) rename integration/{init_chart => init}/stable-mysql/expected/chart/templates/tests/test-configmap.yaml (100%) rename integration/{init_chart => init}/stable-mysql/expected/chart/templates/tests/test.yaml (100%) rename integration/{init_chart => init}/stable-mysql/expected/chart/values.yaml (100%) create mode 100644 integration/init/stable-mysql/expected/overlays/ship/kustomization.yaml create mode 100644 integration/init/stable-mysql/metadata.yaml delete mode 100644 integration/init_chart/stable-mysql/expected/.ship/.helm/.gitkeep delete mode 100644 integration/init_chart/stable-mysql/metadata.yaml delete mode 100644 integration/update/basic/expected/.ship/.helm/.gitkeep delete mode 100644 integration/update/modify-chart/expected/.ship/.helm/.gitkeep delete mode 100644 integration/update/values-static/expected/.ship/.helm/.gitkeep create mode 100644 pkg/specs/apptype/determine_type.go create mode 100644 pkg/specs/constructor.go create mode 100644 pkg/specs/githubclient/client.go create mode 100644 pkg/specs/githubclient/client_test.go create mode 100644 pkg/specs/interface_test.go rename pkg/specs/{ => replicatedapp}/graphql.go (99%) rename pkg/specs/{ => replicatedapp}/resolver.go (82%) rename pkg/specs/{ => replicatedapp}/resolver_test.go (63%) rename pkg/specs/{ => replicatedapp}/selector.go (93%) rename pkg/specs/{ => replicatedapp}/selector_test.go (92%) create mode 100644 pkg/test-mocks/apptype/determine_type_mock.go create mode 100644 pkg/test-mocks/replicatedapp/resolve_replicated_app.go diff --git a/.circleci/config.yml b/.circleci/config.yml index b5ba0b2bf..9fe19bd8d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -116,7 +116,7 @@ jobs: $GOPATH/bin/ginkgo -p -stream integration/base $GOPATH/bin/ginkgo -p -stream integration/update $GOPATH/bin/ginkgo -p -stream integration/init_app - $GOPATH/bin/ginkgo -p -stream integration/init_chart + $GOPATH/bin/ginkgo -p -stream integration/init deploy_unstable: docker: diff --git a/.gitignore b/.gitignore index 0f560b1cb..e3134ee0e 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,7 @@ integration/*/_test_* /.state/ /chart/ +/chart.bak/ /overlays/ /base/ +/base.bak/ diff --git a/Makefile b/Makefile index eb0b8225e..ae2e83852 100644 --- a/Makefile +++ b/Makefile @@ -63,6 +63,8 @@ _mockgen: mkdir -p pkg/test-mocks/daemon mkdir -p pkg/test-mocks/tfplan mkdir -p pkg/test-mocks/state + mkdir -p pkg/test-mocks/apptype + mkdir -p pkg/test-mocks/replicatedapp mockgen \ -destination pkg/test-mocks/ui/ui.go \ -package ui \ @@ -158,6 +160,16 @@ _mockgen: -package lifecycle \ github.com/replicatedhq/ship/pkg/lifecycle \ Renderer + mockgen \ + -destination pkg/test-mocks/apptype/determine_type_mock.go \ + -package apptype \ + github.com/replicatedhq/ship/pkg/specs/apptype \ + Inspector + mockgen \ + -destination pkg/test-mocks/replicatedapp/resolve_replicated_app.go \ + -package replicatedapp \ + github.com/replicatedhq/ship/pkg/specs/replicatedapp \ + Resolver mockgen: _mockgen fmt diff --git a/deploy/e2e.yml b/deploy/e2e.yml deleted file mode 100644 index 4504d06d8..000000000 --- a/deploy/e2e.yml +++ /dev/null @@ -1,18 +0,0 @@ -- name: no config, one inline asset - spec: - assets: - v1: - - inline: - contents: | - #!/bin/sh - kubectl delete ns retraced - dest: ./uninstall.sh - mode: 0777 - lifecycle: - v1: - - config: {} - - render: {} - expect: - ./uninstall.sh: | - #!/bin/sh - kubectl delete ns retraced diff --git a/integration/base/helm-fetch/expected/.ship/.helm/.gitkeep b/integration/base/helm-fetch/expected/.ship/.helm/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/integration/base/helm-nginx/expected/.ship/.helm/.gitkeep b/integration/base/helm-nginx/expected/.ship/.helm/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/integration/base/integration_test.go b/integration/base/integration_test.go index c839ca7b1..b7e92f491 100644 --- a/integration/base/integration_test.go +++ b/integration/base/integration_test.go @@ -31,10 +31,10 @@ type TestMetadata struct { func TestShipApp(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "ship app ...") + RunSpecs(t, "ship app") } -var _ = Describe("ship app ...", func() { +var _ = Describe("ship app", func() { dockerClient, err := client.NewEnvClient() if err != nil { panic(err) diff --git a/integration/init_chart/integration_test.go b/integration/init/integration_test.go similarity index 85% rename from integration/init_chart/integration_test.go rename to integration/init/integration_test.go index 5b80eff9a..4231082a3 100644 --- a/integration/init_chart/integration_test.go +++ b/integration/init/integration_test.go @@ -18,20 +18,19 @@ import ( ) type TestMetadata struct { - Chart string `yaml:"chart"` - Version string `yaml:"version"` - RepoURL string `yaml:"repo_url"` + Upstream string `yaml:"upstream"` + Args []string `yaml:"args"` // debugging SkipCleanup bool `yaml:"skip_cleanup"` } -func TestInitChartWithHelmFetch(t *testing.T) { +func TestInit(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "ship init with helm fetch") + RunSpecs(t, "ship init") } -var _ = Describe("ship init with 'helm fetch'", func() { +var _ = Describe("ship init with arbitrary upstream", func() { dockerClient, err := client.NewEnvClient() if err != nil { panic(err) @@ -81,14 +80,10 @@ var _ = Describe("ship init with 'helm fetch'", func() { cmd.SetOutput(buf) cmd.SetArgs(append([]string{ "init", - testMetadata.Chart, + testMetadata.Upstream, "--headless", "--log-level=off", - "--chart-version", - testMetadata.Version, - "--chart-repo-url", - testMetadata.RepoURL, - })) + }, testMetadata.Args...)) err := cmd.Execute() Expect(err).NotTo(HaveOccurred()) diff --git a/integration/init/plain-k8s/expected/.ship/state.json b/integration/init/plain-k8s/expected/.ship/state.json new file mode 100644 index 000000000..877e91aac --- /dev/null +++ b/integration/init/plain-k8s/expected/.ship/state.json @@ -0,0 +1,7 @@ +{ + "v1": { + "config": {}, + "upstream": "github.com/replicatedhq/test-charts/plain-k8s", + "contentSHA": "7d52a28c5b78539223c3548c2ee677b8b9d624f178e3aac1a15c0097f4e7cb62" + } +} \ No newline at end of file diff --git a/integration/init/plain-k8s/expected/base/MAINTENANCE.md b/integration/init/plain-k8s/expected/base/MAINTENANCE.md new file mode 100644 index 000000000..1de9ecd59 --- /dev/null +++ b/integration/init/plain-k8s/expected/base/MAINTENANCE.md @@ -0,0 +1,23 @@ +## Building the Docker images + +```console +$ docker build -t gcr.io/google-samples/gb-frontend:v5 php-redis + +$ docker build -t gcr.io/google-samples/gb-redisslave:v2 redis-slave +``` + +Building Multi-architecture docker images + +```console +$ make -C php-redis + +$ make -C redis-slave +``` + +Push: + +```console +$ make -C php-redis all-push + +$ make -C redis-slave all-push +``` diff --git a/integration/init/plain-k8s/expected/base/README.md b/integration/init/plain-k8s/expected/base/README.md new file mode 100644 index 000000000..6071deffc --- /dev/null +++ b/integration/init/plain-k8s/expected/base/README.md @@ -0,0 +1,6 @@ +# Example: Guestbook application on Kubernetes + +This directory contains the source code and Kubernetes manifests for PHP +Guestbook application. + +Follow the tutorial at https://kubernetes.io/docs/tutorials/stateless-application/guestbook/. diff --git a/integration/init/plain-k8s/expected/base/frontend-deployment.yaml b/integration/init/plain-k8s/expected/base/frontend-deployment.yaml new file mode 100644 index 000000000..2b08cc9de --- /dev/null +++ b/integration/init/plain-k8s/expected/base/frontend-deployment.yaml @@ -0,0 +1,33 @@ +apiVersion: apps/v1 # for k8s versions before 1.9.0 use apps/v1beta2 and before 1.8.0 use extensions/v1beta1 +kind: Deployment +metadata: + name: frontend +spec: + selector: + matchLabels: + app: guestbook + tier: frontend + replicas: 3 + template: + metadata: + labels: + app: guestbook + tier: frontend + spec: + containers: + - name: php-redis + image: gcr.io/google-samples/gb-frontend:v4 + resources: + requests: + cpu: 100m + memory: 100Mi + env: + - name: GET_HOSTS_FROM + value: dns + # If your cluster config does not include a dns service, then to + # instead access environment variables to find service host + # info, comment out the 'value: dns' line above, and uncomment the + # line below: + # value: env + ports: + - containerPort: 80 diff --git a/integration/init/plain-k8s/expected/base/frontend-service.yaml b/integration/init/plain-k8s/expected/base/frontend-service.yaml new file mode 100644 index 000000000..6f283f347 --- /dev/null +++ b/integration/init/plain-k8s/expected/base/frontend-service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: frontend + labels: + app: guestbook + tier: frontend +spec: + # comment or delete the following line if you want to use a LoadBalancer + type: NodePort + # if your cluster supports it, uncomment the following to automatically create + # an external load-balanced IP for the frontend service. + # type: LoadBalancer + ports: + - port: 80 + selector: + app: guestbook + tier: frontend diff --git a/integration/init/plain-k8s/expected/base/kustomization.yaml b/integration/init/plain-k8s/expected/base/kustomization.yaml new file mode 100644 index 000000000..491cedc81 --- /dev/null +++ b/integration/init/plain-k8s/expected/base/kustomization.yaml @@ -0,0 +1,7 @@ +resources: +- frontend-deployment.yaml +- frontend-service.yaml +- redis-master-deployment.yaml +- redis-master-service.yaml +- redis-slave-deployment.yaml +- redis-slave-service.yaml diff --git a/integration/init/plain-k8s/expected/base/redis-master-deployment.yaml b/integration/init/plain-k8s/expected/base/redis-master-deployment.yaml new file mode 100644 index 000000000..8d4701c04 --- /dev/null +++ b/integration/init/plain-k8s/expected/base/redis-master-deployment.yaml @@ -0,0 +1,27 @@ +apiVersion: apps/v1 # for k8s versions before 1.9.0 use apps/v1beta2 and before 1.8.0 use extensions/v1beta1 +kind: Deployment +metadata: + name: redis-master +spec: + selector: + matchLabels: + app: redis + role: master + tier: backend + replicas: 1 + template: + metadata: + labels: + app: redis + role: master + tier: backend + spec: + containers: + - name: master + image: k8s.gcr.io/redis:e2e # or just image: redis + resources: + requests: + cpu: 100m + memory: 100Mi + ports: + - containerPort: 6379 diff --git a/integration/init/plain-k8s/expected/base/redis-master-service.yaml b/integration/init/plain-k8s/expected/base/redis-master-service.yaml new file mode 100644 index 000000000..a484014f1 --- /dev/null +++ b/integration/init/plain-k8s/expected/base/redis-master-service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: redis-master + labels: + app: redis + role: master + tier: backend +spec: + ports: + - port: 6379 + targetPort: 6379 + selector: + app: redis + role: master + tier: backend diff --git a/integration/init/plain-k8s/expected/base/redis-slave-deployment.yaml b/integration/init/plain-k8s/expected/base/redis-slave-deployment.yaml new file mode 100644 index 000000000..e674f1d44 --- /dev/null +++ b/integration/init/plain-k8s/expected/base/redis-slave-deployment.yaml @@ -0,0 +1,35 @@ +apiVersion: apps/v1 # for k8s versions before 1.9.0 use apps/v1beta2 and before 1.8.0 use extensions/v1beta1 +kind: Deployment +metadata: + name: redis-slave +spec: + selector: + matchLabels: + app: redis + role: slave + tier: backend + replicas: 2 + template: + metadata: + labels: + app: redis + role: slave + tier: backend + spec: + containers: + - name: slave + image: gcr.io/google_samples/gb-redisslave:v1 + resources: + requests: + cpu: 100m + memory: 100Mi + env: + - name: GET_HOSTS_FROM + value: dns + # If your cluster config does not include a dns service, then to + # instead access an environment variable to find the master + # service's host, comment out the 'value: dns' line above, and + # uncomment the line below: + # value: env + ports: + - containerPort: 6379 diff --git a/integration/init/plain-k8s/expected/base/redis-slave-service.yaml b/integration/init/plain-k8s/expected/base/redis-slave-service.yaml new file mode 100644 index 000000000..238fd63fb --- /dev/null +++ b/integration/init/plain-k8s/expected/base/redis-slave-service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: redis-slave + labels: + app: redis + role: slave + tier: backend +spec: + ports: + - port: 6379 + selector: + app: redis + role: slave + tier: backend diff --git a/integration/init_chart/stable-mysql/expected/overlays/ship/kustomization.yaml b/integration/init/plain-k8s/expected/overlays/ship/kustomization.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/overlays/ship/kustomization.yaml rename to integration/init/plain-k8s/expected/overlays/ship/kustomization.yaml diff --git a/integration/init/plain-k8s/metadata.yaml b/integration/init/plain-k8s/metadata.yaml new file mode 100644 index 000000000..93cc47001 --- /dev/null +++ b/integration/init/plain-k8s/metadata.yaml @@ -0,0 +1,3 @@ +upstream: "github.com/replicatedhq/test-charts/plain-k8s" +args: [] +skip_cleanup: false diff --git a/integration/init_chart/stable-mysql/expected/.ship/state.json b/integration/init/stable-mysql/expected/.ship/state.json similarity index 82% rename from integration/init_chart/stable-mysql/expected/.ship/state.json rename to integration/init/stable-mysql/expected/.ship/state.json index d0891777d..ab70c1895 100644 --- a/integration/init_chart/stable-mysql/expected/.ship/state.json +++ b/integration/init/stable-mysql/expected/.ship/state.json @@ -1,7 +1,7 @@ { "v1": { "config": {}, - "chartURL": "stable/mysql", + "upstream": "stable/mysql", "ChartVersion": "0.8.1", "contentSHA": "58d9ac4317f420275e1e05f1144eeadbfdcdeff373762625d58001a42e013092" } diff --git a/integration/init_chart/stable-mysql/expected/base/deployment.yaml b/integration/init/stable-mysql/expected/base/deployment.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/base/deployment.yaml rename to integration/init/stable-mysql/expected/base/deployment.yaml diff --git a/integration/init_chart/stable-mysql/expected/base/kustomization.yaml b/integration/init/stable-mysql/expected/base/kustomization.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/base/kustomization.yaml rename to integration/init/stable-mysql/expected/base/kustomization.yaml diff --git a/integration/init_chart/stable-mysql/expected/base/pvc.yaml b/integration/init/stable-mysql/expected/base/pvc.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/base/pvc.yaml rename to integration/init/stable-mysql/expected/base/pvc.yaml diff --git a/integration/init_chart/stable-mysql/expected/base/secrets.yaml b/integration/init/stable-mysql/expected/base/secrets.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/base/secrets.yaml rename to integration/init/stable-mysql/expected/base/secrets.yaml diff --git a/integration/init_chart/stable-mysql/expected/base/svc.yaml b/integration/init/stable-mysql/expected/base/svc.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/base/svc.yaml rename to integration/init/stable-mysql/expected/base/svc.yaml diff --git a/integration/init_chart/stable-mysql/expected/base/tests/test-configmap.yaml b/integration/init/stable-mysql/expected/base/tests/test-configmap.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/base/tests/test-configmap.yaml rename to integration/init/stable-mysql/expected/base/tests/test-configmap.yaml diff --git a/integration/init_chart/stable-mysql/expected/base/tests/test.yaml b/integration/init/stable-mysql/expected/base/tests/test.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/base/tests/test.yaml rename to integration/init/stable-mysql/expected/base/tests/test.yaml diff --git a/integration/init_chart/stable-mysql/expected/chart/.helmignore b/integration/init/stable-mysql/expected/chart/.helmignore similarity index 100% rename from integration/init_chart/stable-mysql/expected/chart/.helmignore rename to integration/init/stable-mysql/expected/chart/.helmignore diff --git a/integration/init_chart/stable-mysql/expected/chart/Chart.yaml b/integration/init/stable-mysql/expected/chart/Chart.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/chart/Chart.yaml rename to integration/init/stable-mysql/expected/chart/Chart.yaml diff --git a/integration/init_chart/stable-mysql/expected/chart/README.md b/integration/init/stable-mysql/expected/chart/README.md similarity index 100% rename from integration/init_chart/stable-mysql/expected/chart/README.md rename to integration/init/stable-mysql/expected/chart/README.md diff --git a/integration/init_chart/stable-mysql/expected/chart/templates/NOTES.txt b/integration/init/stable-mysql/expected/chart/templates/NOTES.txt similarity index 100% rename from integration/init_chart/stable-mysql/expected/chart/templates/NOTES.txt rename to integration/init/stable-mysql/expected/chart/templates/NOTES.txt diff --git a/integration/init_chart/stable-mysql/expected/chart/templates/_helpers.tpl b/integration/init/stable-mysql/expected/chart/templates/_helpers.tpl similarity index 100% rename from integration/init_chart/stable-mysql/expected/chart/templates/_helpers.tpl rename to integration/init/stable-mysql/expected/chart/templates/_helpers.tpl diff --git a/integration/init_chart/stable-mysql/expected/chart/templates/configurationFiles-configmap.yaml b/integration/init/stable-mysql/expected/chart/templates/configurationFiles-configmap.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/chart/templates/configurationFiles-configmap.yaml rename to integration/init/stable-mysql/expected/chart/templates/configurationFiles-configmap.yaml diff --git a/integration/init_chart/stable-mysql/expected/chart/templates/deployment.yaml b/integration/init/stable-mysql/expected/chart/templates/deployment.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/chart/templates/deployment.yaml rename to integration/init/stable-mysql/expected/chart/templates/deployment.yaml diff --git a/integration/init_chart/stable-mysql/expected/chart/templates/initializationFiles-configmap.yaml b/integration/init/stable-mysql/expected/chart/templates/initializationFiles-configmap.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/chart/templates/initializationFiles-configmap.yaml rename to integration/init/stable-mysql/expected/chart/templates/initializationFiles-configmap.yaml diff --git a/integration/init_chart/stable-mysql/expected/chart/templates/pvc.yaml b/integration/init/stable-mysql/expected/chart/templates/pvc.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/chart/templates/pvc.yaml rename to integration/init/stable-mysql/expected/chart/templates/pvc.yaml diff --git a/integration/init_chart/stable-mysql/expected/chart/templates/secrets.yaml b/integration/init/stable-mysql/expected/chart/templates/secrets.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/chart/templates/secrets.yaml rename to integration/init/stable-mysql/expected/chart/templates/secrets.yaml diff --git a/integration/init_chart/stable-mysql/expected/chart/templates/svc.yaml b/integration/init/stable-mysql/expected/chart/templates/svc.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/chart/templates/svc.yaml rename to integration/init/stable-mysql/expected/chart/templates/svc.yaml diff --git a/integration/init_chart/stable-mysql/expected/chart/templates/tests/test-configmap.yaml b/integration/init/stable-mysql/expected/chart/templates/tests/test-configmap.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/chart/templates/tests/test-configmap.yaml rename to integration/init/stable-mysql/expected/chart/templates/tests/test-configmap.yaml diff --git a/integration/init_chart/stable-mysql/expected/chart/templates/tests/test.yaml b/integration/init/stable-mysql/expected/chart/templates/tests/test.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/chart/templates/tests/test.yaml rename to integration/init/stable-mysql/expected/chart/templates/tests/test.yaml diff --git a/integration/init_chart/stable-mysql/expected/chart/values.yaml b/integration/init/stable-mysql/expected/chart/values.yaml similarity index 100% rename from integration/init_chart/stable-mysql/expected/chart/values.yaml rename to integration/init/stable-mysql/expected/chart/values.yaml diff --git a/integration/init/stable-mysql/expected/overlays/ship/kustomization.yaml b/integration/init/stable-mysql/expected/overlays/ship/kustomization.yaml new file mode 100644 index 000000000..a3fd1e4e6 --- /dev/null +++ b/integration/init/stable-mysql/expected/overlays/ship/kustomization.yaml @@ -0,0 +1,2 @@ +bases: +- ../../base diff --git a/integration/init/stable-mysql/metadata.yaml b/integration/init/stable-mysql/metadata.yaml new file mode 100644 index 000000000..6e90f5f07 --- /dev/null +++ b/integration/init/stable-mysql/metadata.yaml @@ -0,0 +1,5 @@ +upstream: "stable/mysql" +args: + - --chart-version=0.8.1 + + diff --git a/integration/init_app/integration_test.go b/integration/init_app/integration_test.go index d0d710da8..e2879ebb8 100644 --- a/integration/init_app/integration_test.go +++ b/integration/init_app/integration_test.go @@ -109,15 +109,15 @@ var _ = Describe("ship init replicated.app/...", func() { } isStaging := strings.Contains(customerEndpoint, "staging") - initTarget := "replicated.app/some-cool-ci-tool" + upstream := "replicated.app/some-cool-ci-tool" if isStaging { - initTarget = "staging.replicated.app/some-cool-ci-tool" + upstream = "staging.replicated.app/some-cool-ci-tool" } // this should probably be url encoded but whatever - initTarget = fmt.Sprintf( + upstream = fmt.Sprintf( "%s?installation_id=%s&customer_id=%s", - initTarget, + upstream, installationID, testMetadata.CustomerID, ) @@ -127,7 +127,7 @@ var _ = Describe("ship init replicated.app/...", func() { cmd.SetOutput(buf) cmd.SetArgs(append([]string{ "init", - initTarget, + upstream, "--headless", fmt.Sprintf("--state-file=%s", path.Join(testInputPath, ".ship/state.json")), "--log-level=off", diff --git a/integration/init_chart/stable-mysql/expected/.ship/.helm/.gitkeep b/integration/init_chart/stable-mysql/expected/.ship/.helm/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/integration/init_chart/stable-mysql/metadata.yaml b/integration/init_chart/stable-mysql/metadata.yaml deleted file mode 100644 index 1833ded81..000000000 --- a/integration/init_chart/stable-mysql/metadata.yaml +++ /dev/null @@ -1,3 +0,0 @@ -chart: "stable/mysql" -version: "0.8.1" -skip_cleanup: false \ No newline at end of file diff --git a/integration/update/basic/expected/.ship/.helm/.gitkeep b/integration/update/basic/expected/.ship/.helm/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/integration/update/basic/expected/.ship/state.json b/integration/update/basic/expected/.ship/state.json index fe90fe501..79c10556a 100644 --- a/integration/update/basic/expected/.ship/state.json +++ b/integration/update/basic/expected/.ship/state.json @@ -1 +1 @@ -{"v1":{"config":{},"helmValues":"# Default values for basic.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\nreplicaCount: 5\n\nimage:\n repository: nginx\n tag: stable\n pullPolicy: IfNotPresent\n\nservice:\n type: ClusterIP\n port: 80\n\ningress:\n enabled: false\n annotations: {}\n # kubernetes.io/ingress.class: nginx\n # kubernetes.io/tls-acme: \"true\"\n path: /\n hosts:\n - chart-example.local\n tls: []\n # - secretName: chart-example-tls\n # hosts:\n # - chart-example.local\n\nresources: {}\n # We usually recommend not to specify default resources and to leave this as a conscious\n # choice for the user. This also increases chances charts run on environments with little\n # resources, such as Minikube. If you do want to specify resources, uncomment the following\n # lines, adjust them as necessary, and remove the curly braces after 'resources:'.\n # limits:\n # cpu: 100m\n # memory: 128Mi\n # requests:\n # cpu: 100m\n # memory: 128Mi\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n","kustomize":{"overlays":{"ship":{"patches":{"/templates/deployment.yaml":"--- \napiVersion: apps/v1beta2\nkind: Deployment\nmetadata:\n name: 'basic'\n"}}}},"chartURL":"github.com/replicatedhq/test-charts/basic","contentSHA":"94d255c48a20929dbfbe341109a7ab86dae1ecfab3b8f01151bb36a2ebea7bbe"}} \ No newline at end of file +{"v1":{"config":{},"helmValues":"# Default values for basic.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\nreplicaCount: 5\n\nimage:\n repository: nginx\n tag: stable\n pullPolicy: IfNotPresent\n\nservice:\n type: ClusterIP\n port: 80\n\ningress:\n enabled: false\n annotations: {}\n # kubernetes.io/ingress.class: nginx\n # kubernetes.io/tls-acme: \"true\"\n path: /\n hosts:\n - chart-example.local\n tls: []\n # - secretName: chart-example-tls\n # hosts:\n # - chart-example.local\n\nresources: {}\n # We usually recommend not to specify default resources and to leave this as a conscious\n # choice for the user. This also increases chances charts run on environments with little\n # resources, such as Minikube. If you do want to specify resources, uncomment the following\n # lines, adjust them as necessary, and remove the curly braces after 'resources:'.\n # limits:\n # cpu: 100m\n # memory: 128Mi\n # requests:\n # cpu: 100m\n # memory: 128Mi\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n","kustomize":{"overlays":{"ship":{"patches":{"/templates/deployment.yaml":"--- \napiVersion: apps/v1beta2\nkind: Deployment\nmetadata:\n name: 'basic'\n"}}}},"upstream":"github.com/replicatedhq/test-charts/basic","contentSHA":"94d255c48a20929dbfbe341109a7ab86dae1ecfab3b8f01151bb36a2ebea7bbe"}} \ No newline at end of file diff --git a/integration/update/basic/input/.ship/state.json b/integration/update/basic/input/.ship/state.json index d552cfe7c..e6ff0d514 100644 --- a/integration/update/basic/input/.ship/state.json +++ b/integration/update/basic/input/.ship/state.json @@ -1 +1 @@ -{"v1":{"config":{},"helmValues":"# Default values for basic.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\nreplicaCount: 5\n\nimage:\n repository: nginx\n tag: stable\n pullPolicy: IfNotPresent\n\nservice:\n type: ClusterIP\n port: 80\n\ningress:\n enabled: false\n annotations: {}\n # kubernetes.io/ingress.class: nginx\n # kubernetes.io/tls-acme: \"true\"\n path: /\n hosts:\n - chart-example.local\n tls: []\n # - secretName: chart-example-tls\n # hosts:\n # - chart-example.local\n\nresources: {}\n # We usually recommend not to specify default resources and to leave this as a conscious\n # choice for the user. This also increases chances charts run on environments with little\n # resources, such as Minikube. If you do want to specify resources, uncomment the following\n # lines, adjust them as necessary, and remove the curly braces after 'resources:'.\n # limits:\n # cpu: 100m\n # memory: 128Mi\n # requests:\n # cpu: 100m\n # memory: 128Mi\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n","kustomize":{"overlays":{"ship":{"patches":{"/templates/deployment.yaml":"--- \napiVersion: apps/v1beta2\nkind: Deployment\nmetadata:\n name: 'basic'\n"}}}},"chartURL":"github.com/replicatedhq/test-charts/basic"}} \ No newline at end of file +{"v1":{"config":{},"helmValues":"# Default values for basic.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\nreplicaCount: 5\n\nimage:\n repository: nginx\n tag: stable\n pullPolicy: IfNotPresent\n\nservice:\n type: ClusterIP\n port: 80\n\ningress:\n enabled: false\n annotations: {}\n # kubernetes.io/ingress.class: nginx\n # kubernetes.io/tls-acme: \"true\"\n path: /\n hosts:\n - chart-example.local\n tls: []\n # - secretName: chart-example-tls\n # hosts:\n # - chart-example.local\n\nresources: {}\n # We usually recommend not to specify default resources and to leave this as a conscious\n # choice for the user. This also increases chances charts run on environments with little\n # resources, such as Minikube. If you do want to specify resources, uncomment the following\n # lines, adjust them as necessary, and remove the curly braces after 'resources:'.\n # limits:\n # cpu: 100m\n # memory: 128Mi\n # requests:\n # cpu: 100m\n # memory: 128Mi\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n","kustomize":{"overlays":{"ship":{"patches":{"/templates/deployment.yaml":"--- \napiVersion: apps/v1beta2\nkind: Deployment\nmetadata:\n name: 'basic'\n"}}}},"upstream":"github.com/replicatedhq/test-charts/basic"}} \ No newline at end of file diff --git a/integration/update/modify-chart/expected/.ship/.helm/.gitkeep b/integration/update/modify-chart/expected/.ship/.helm/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/integration/update/modify-chart/expected/.ship/state.json b/integration/update/modify-chart/expected/.ship/state.json index 857561fbc..de5a4f777 100644 --- a/integration/update/modify-chart/expected/.ship/state.json +++ b/integration/update/modify-chart/expected/.ship/state.json @@ -1 +1,17 @@ -{"v1":{"config":{},"helmValues":"# Default values for modify-chart.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\nreplicaCount: 1\n\nimage:\n repository: nginx\n tag: stable\n pullPolicy: IfNotPresent\n\nservice:\n type: ClusterIP\n port: 80\n\ningress:\n enabled: false\n annotations: {}\n # kubernetes.io/ingress.class: nginx\n # kubernetes.io/tls-acme: \"true\"\n path: /\n hosts:\n - chart-example.local\n tls: []\n # - secretName: chart-example-tls\n # hosts:\n # - chart-example.local\n\nresources: {}\n # We usually recommend not to specify default resources and to leave this as a conscious\n # choice for the user. This also increases chances charts run on environments with little\n # resources, such as Minikube. If you do want to specify resources, uncomment the following\n # lines, adjust them as necessary, and remove the curly braces after 'resources:'.\n # limits:\n # cpu: 100m\n # memory: 128Mi\n # requests:\n # cpu: 100m\n # memory: 128Mi\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n","kustomize":{"overlays":{"ship":{"patches":{"/templates/deployment.yaml":""}}}},"chartURL":"github.com/replicatedhq/test-charts/modify-chart","contentSHA":"8f221c20ea01b45298094e4c631d10b7583885fd4a62a5eacce2883cd19f1c7f"}} \ No newline at end of file +{ + "v1": { + "config": {}, + "helmValues": "# Default values for modify-chart.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\nreplicaCount: 1\n\nimage:\n repository: nginx\n tag: stable\n pullPolicy: IfNotPresent\n\nservice:\n type: ClusterIP\n port: 80\n\ningress:\n enabled: false\n annotations: {}\n # kubernetes.io/ingress.class: nginx\n # kubernetes.io/tls-acme: \"true\"\n path: /\n hosts:\n - chart-example.local\n tls: []\n # - secretName: chart-example-tls\n # hosts:\n # - chart-example.local\n\nresources: {}\n # We usually recommend not to specify default resources and to leave this as a conscious\n # choice for the user. This also increases chances charts run on environments with little\n # resources, such as Minikube. If you do want to specify resources, uncomment the following\n # lines, adjust them as necessary, and remove the curly braces after 'resources:'.\n # limits:\n # cpu: 100m\n # memory: 128Mi\n # requests:\n # cpu: 100m\n # memory: 128Mi\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n", + "kustomize": { + "overlays": { + "ship": { + "patches": { + "/templates/deployment.yaml": "" + } + } + } + }, + "upstream": "github.com/replicatedhq/test-charts/modify-chart", + "contentSHA": "8f221c20ea01b45298094e4c631d10b7583885fd4a62a5eacce2883cd19f1c7f" + } +} \ No newline at end of file diff --git a/integration/update/modify-chart/input/.ship/state.json b/integration/update/modify-chart/input/.ship/state.json index e281f7ad4..f22aa921c 100644 --- a/integration/update/modify-chart/input/.ship/state.json +++ b/integration/update/modify-chart/input/.ship/state.json @@ -11,6 +11,6 @@ } } }, - "chartURL": "github.com/replicatedhq/test-charts/modify-chart" + "upstream": "github.com/replicatedhq/test-charts/modify-chart" } } \ No newline at end of file diff --git a/integration/update/modify-chart/metadata.yaml b/integration/update/modify-chart/metadata.yaml index b05d203aa..6011ed1cd 100644 --- a/integration/update/modify-chart/metadata.yaml +++ b/integration/update/modify-chart/metadata.yaml @@ -1,2 +1,3 @@ args: [] +skip_cleanup: false diff --git a/integration/update/values-static/expected/.ship/.helm/.gitkeep b/integration/update/values-static/expected/.ship/.helm/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/integration/update/values-static/expected/.ship/state.json b/integration/update/values-static/expected/.ship/state.json index d4b00133d..99dce294f 100644 --- a/integration/update/values-static/expected/.ship/state.json +++ b/integration/update/values-static/expected/.ship/state.json @@ -1 +1,17 @@ -{"v1":{"config":{},"helmValues":"replicaCount: 2\nimage:\n repository: nginx\n tag: stable\n\n","kustomize":{"overlays":{"ship":{"patches":{"/deployment.yaml":"apiVersion: apps/v1beta2\nkind: Deployment\nmetadata:\n labels:\n app: my-app\n chart: values-update\n heritage: Tiller\n release: values-update\n name: deployment\nspec:\n template:\n spec:\n $setElementOrder/containers:\n - name: values-update\n containers:\n - imagePullPolicy: IfNotPresent\n name: values-update\n"}}}},"chartURL":"github.com/replicatedhq/test-charts/values-static","contentSHA":"77129bbe0cc210b60ae3a2a5775f79a7888d444a2081e6e4f190ebaf2a8da53d","lifecycle":{"stepsCompleted":{"intro":true,"kustomize":true,"kustomize-intro":true,"outro":true,"render":true,"values":true}}}} +{ + "v1": { + "config": {}, + "helmValues": "replicaCount: 2\nimage:\n repository: nginx\n tag: stable\n\n", + "kustomize": { + "overlays": { + "ship": { + "patches": { + "/deployment.yaml": "apiVersion: apps/v1beta2\nkind: Deployment\nmetadata:\n labels:\n app: my-app\n chart: values-update\n heritage: Tiller\n release: values-update\n name: deployment\nspec:\n template:\n spec:\n $setElementOrder/containers:\n - name: values-update\n containers:\n - imagePullPolicy: IfNotPresent\n name: values-update\n" + } + } + } + }, + "upstream": "github.com/replicatedhq/test-charts/values-static", + "contentSHA": "77129bbe0cc210b60ae3a2a5775f79a7888d444a2081e6e4f190ebaf2a8da53d" + } +} diff --git a/integration/update/values-static/input/.ship/state.json b/integration/update/values-static/input/.ship/state.json index a69822560..a110f38cb 100644 --- a/integration/update/values-static/input/.ship/state.json +++ b/integration/update/values-static/input/.ship/state.json @@ -1 +1,17 @@ -{"v1":{"config":null,"helmValues":"replicaCount: 2\nimage:\n repository: nginx\n tag: stable\n\n","kustomize":{"overlays":{"ship":{"patches":{"/deployment.yaml":"apiVersion: apps/v1beta2\nkind: Deployment\nmetadata:\n labels:\n app: my-app\n chart: values-update\n heritage: Tiller\n release: values-update\n name: deployment\nspec:\n template:\n spec:\n $setElementOrder/containers:\n - name: values-update\n containers:\n - imagePullPolicy: IfNotPresent\n name: values-update\n"}}}},"chartURL":"github.com/replicatedhq/test-charts/values-static","contentSHA":"77129bbe0cc210b60ae3a2a5775f79a7888d444a2081e6e4f190ebaf2a8da53d","lifecycle":{"stepsCompleted":{"intro":true,"kustomize":true,"kustomize-intro":true,"outro":true,"render":true,"values":true}}}} \ No newline at end of file +{ + "v1": { + "config": null, + "helmValues": "replicaCount: 2\nimage:\n repository: nginx\n tag: stable\n\n", + "kustomize": { + "overlays": { + "ship": { + "patches": { + "/deployment.yaml": "apiVersion: apps/v1beta2\nkind: Deployment\nmetadata:\n labels:\n app: my-app\n chart: values-update\n heritage: Tiller\n release: values-update\n name: deployment\nspec:\n template:\n spec:\n $setElementOrder/containers:\n - name: values-update\n containers:\n - imagePullPolicy: IfNotPresent\n name: values-update\n" + } + } + } + }, + "upstream": "github.com/replicatedhq/test-charts/values-static", + "contentSHA": "77129bbe0cc210b60ae3a2a5775f79a7888d444a2081e6e4f190ebaf2a8da53d" + } +} \ No newline at end of file diff --git a/integration/update/values-update/expected/.ship/state.json b/integration/update/values-update/expected/.ship/state.json index 2ca316f0b..47b5cb511 100644 --- a/integration/update/values-update/expected/.ship/state.json +++ b/integration/update/values-update/expected/.ship/state.json @@ -11,7 +11,7 @@ } } }, - "chartURL": "github.com/replicatedhq/test-charts/values-update", + "upstream": "github.com/replicatedhq/test-charts/values-update", "contentSHA": "0dff27ff75463c7d4a4a98833c754fd1dde2a76bdbead597939ad80bdb8b5e67", "lifecycle": { "stepsCompleted": { diff --git a/integration/update/values-update/input/.ship/state.json b/integration/update/values-update/input/.ship/state.json index d9b54eee0..997b293ef 100644 --- a/integration/update/values-update/input/.ship/state.json +++ b/integration/update/values-update/input/.ship/state.json @@ -11,17 +11,7 @@ } } }, - "chartURL": "github.com/replicatedhq/test-charts/values-update", - "contentSHA": "77129bbe0cc210b60ae3a2a5775f79a7888d444a2081e6e4f190ebaf2a8da53d", - "lifecycle": { - "stepsCompleted": { - "intro": true, - "kustomize": true, - "kustomize-intro": true, - "outro": true, - "render": true, - "values": true - } - } + "upstream": "github.com/replicatedhq/test-charts/values-update", + "contentSHA": "77129bbe0cc210b60ae3a2a5775f79a7888d444a2081e6e4f190ebaf2a8da53d" } } \ No newline at end of file diff --git a/pkg/api/spec.go b/pkg/api/spec.go index 72bed530e..f5fc5d677 100644 --- a/pkg/api/spec.go +++ b/pkg/api/spec.go @@ -31,7 +31,7 @@ type GithubFile struct { Data string `json:"data" yaml:"data" hcl:"data" meta:"data"` } -type HelmChartMetadata struct { +type ShipAppMetadata struct { Description string `json:"description" yaml:"description" hcl:"description" meta:"description"` Version string `json:"version" yaml:"version" hcl:"version" meta:"version"` Icon string `json:"icon" yaml:"icon" hcl:"icon" meta:"icon"` @@ -43,18 +43,18 @@ type HelmChartMetadata struct { // ReleaseMetadata type ReleaseMetadata struct { - ReleaseID string `json:"releaseId" yaml:"releaseId" hcl:"releaseId" meta:"release-id"` - CustomerID string `json:"customerId" yaml:"customerId" hcl:"customerId" meta:"customer-id"` - ChannelID string `json:"channelId" yaml:"channelId" hcl:"channelId" meta:"channel-id"` - ChannelName string `json:"channelName" yaml:"channelName" hcl:"channelName" meta:"channel-name"` - ChannelIcon string `json:"channelIcon" yaml:"channelIcon" hcl:"channelIcon" meta:"channel-icon"` - Semver string `json:"semver" yaml:"semver" hcl:"semver" meta:"release-version"` - ReleaseNotes string `json:"releaseNotes" yaml:"releaseNotes" hcl:"releaseNotes" meta:"release-notes"` - Created string `json:"created" yaml:"created" hcl:"created" meta:"release-date"` - RegistrySecret string `json:"registrySecret" yaml:"registrySecret" hcl:"registrySecret" meta:"registry-secret"` - Images []Image `json:"images" yaml:"images" hcl:"images" meta:"images"` - GithubContents []GithubContent `json:"githubContents" yaml:"githubContents" hcl:"githubContents" meta:"githubContents"` - HelmChartMetadata HelmChartMetadata `json:"helmChartMetadata" yaml:"helmChartMetadata" hcl:"helmChartMetadata" meta:"helmChartMetadata"` + ReleaseID string `json:"releaseId" yaml:"releaseId" hcl:"releaseId" meta:"release-id"` + CustomerID string `json:"customerId" yaml:"customerId" hcl:"customerId" meta:"customer-id"` + ChannelID string `json:"channelId" yaml:"channelId" hcl:"channelId" meta:"channel-id"` + ChannelName string `json:"channelName" yaml:"channelName" hcl:"channelName" meta:"channel-name"` + ChannelIcon string `json:"channelIcon" yaml:"channelIcon" hcl:"channelIcon" meta:"channel-icon"` + Semver string `json:"semver" yaml:"semver" hcl:"semver" meta:"release-version"` + ReleaseNotes string `json:"releaseNotes" yaml:"releaseNotes" hcl:"releaseNotes" meta:"release-notes"` + Created string `json:"created" yaml:"created" hcl:"created" meta:"release-date"` + RegistrySecret string `json:"registrySecret" yaml:"registrySecret" hcl:"registrySecret" meta:"registry-secret"` + Images []Image `json:"images" yaml:"images" hcl:"images" meta:"images"` + GithubContents []GithubContent `json:"githubContents" yaml:"githubContents" hcl:"githubContents" meta:"githubContents"` + ShipAppMetadata ShipAppMetadata `json:"shipAppMetadata" yaml:"shipAppMetadata" hcl:"shipAppMetadata" meta:"shipAppMetadata"` } func (r *ReleaseMetadata) ReleaseName() string { @@ -62,8 +62,8 @@ func (r *ReleaseMetadata) ReleaseName() string { return r.ChannelName } - if r.HelmChartMetadata.Name != "" { - return r.HelmChartMetadata.Name + if r.ShipAppMetadata.Name != "" { + return r.ShipAppMetadata.Name } return "ship" diff --git a/pkg/cli/root.go b/pkg/cli/root.go index e83c17b1a..a299dbd35 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -30,6 +30,14 @@ func RootCmd() *cobra.Command { cmd.Help() os.Exit(1) }, + // I think its okay to use real OS filesystem commands instead of afero here, + // since I think cobra lives outside the scope of dig injection/unit testing. + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + return os.MkdirAll(constants.ShipPathInternalTmp, 0755) + }, + PersistentPostRunE: func(cmd *cobra.Command, args []string) error { + return os.RemoveAll(constants.ShipPathInternalTmp) + }, } cobra.OnInitialize(initConfig) diff --git a/pkg/constants/filepaths.go b/pkg/constants/filepaths.go index baada9708..21b855d48 100644 --- a/pkg/constants/filepaths.go +++ b/pkg/constants/filepaths.go @@ -1,34 +1,31 @@ package constants -// InstallerPrefixPath is the path prefix of installed assets -const InstallerPrefixPath = "installer" - -// ShipPath is the default folder path of Ship configuration -const ShipPath = ".ship" - -// OverlaysPrefixPath is the path prefix of overlays -const OverlaysPrefixPath = "overlays/ship" - -// StatePath is the default state file path -const StatePath = ".ship/state.json" - -// ReleasePath is the default place to write a pulled release to the filesystem -const ReleasePath = ".ship/release.yml" - -// KustomizeHelmPath is the path used to store Helm chart contents -const KustomizeHelmPath = "chart" - -// RenderedHelmTempPath is the path where the `helm template` command writes to -const RenderedHelmTempPath = ".ship/tmp-rendered" - -// RenderedHelmPath is the path where rendered Helm charts are written to -const RenderedHelmPath = "base" - -// TempHelmValuesPath is the folder path used to store the updated values.yaml -const TempHelmValuesPath = "chart/tmp" - -// TempApplyOverlayPath is the folder path used to apply patch -const TempApplyOverlayPath = "overlays/tmp-apply" - -// TempHelmHomePath is the path helm will use as its home directory -const TempHelmHomePath = ".ship/.helm" +import "path" + +const ( + // InstallerPrefixPath is the path prefix of installed assets + InstallerPrefixPath = "installer" + // ShipPathInternal is the default folder path of Ship configuration + ShipPathInternal = ".ship" + // HelmChartPath is the path used to store Helm chart contents + HelmChartPath = "chart" + // RenderedHelmTempPath is the path where the `helm template` command writes to + RenderedHelmTempPath = "tmp-rendered" + // KustomizeBasePath is the path to which assets to be kustomized are written + KustomizeBasePath = "base" +) + +var ( + // ShipPathInternalTmp is a temporary folder that will get cleaned up on exit + ShipPathInternalTmp = path.Join(".ship", "tmp") + // InternalTempHelmHome is the path to a helm home directory + InternalTempHelmHome = path.Join(ShipPathInternalTmp, ".helm") + // StatePath is the default state file path + StatePath = path.Join(ShipPathInternal, "state.json") + // ReleasePath is the default place to write a pulled release to the filesystem + ReleasePath = path.Join(ShipPathInternal, "release.yml") + // TempHelmValuesPath is the folder path used to store the updated values.yaml + TempHelmValuesPath = path.Join(HelmChartPath, "tmp") + // TempApplyOverlayPath is the folder path used to apply patch + TempApplyOverlayPath = path.Join("overlays", "tmp-apply") +) diff --git a/pkg/helm/shared.go b/pkg/helm/shared.go index 26050719e..606d5510c 100644 --- a/pkg/helm/shared.go +++ b/pkg/helm/shared.go @@ -219,5 +219,5 @@ func prettyError(err error) error { } func helmHome() (string, error) { - return constants.TempHelmHomePath, nil + return constants.InternalTempHelmHome, nil } diff --git a/pkg/lifecycle/daemon/headless/daemon.go b/pkg/lifecycle/daemon/headless/daemon.go index e11bcaeac..4a25c6eda 100644 --- a/pkg/lifecycle/daemon/headless/daemon.go +++ b/pkg/lifecycle/daemon/headless/daemon.go @@ -72,7 +72,7 @@ func (d *HeadlessDaemon) PushHelmValuesStep(ctx context.Context, helmValues daem func (d *HeadlessDaemon) HeadlessSaveHelmValues(ctx context.Context, helmValues string) error { warn := level.Warn(log.With(d.Logger, "struct", "HeadlessDaemon", "method", "HeadlessSaveHelmValues")) - chartDefaultValues, err := d.FS.ReadFile(path.Join(constants.KustomizeHelmPath, "values.yaml")) + chartDefaultValues, err := d.FS.ReadFile(path.Join(constants.HelmChartPath, "values.yaml")) if err != nil { return errors.Wrap(err, "load chart defaults") } diff --git a/pkg/lifecycle/daemon/routes_navcycle_completestep_test.go b/pkg/lifecycle/daemon/routes_navcycle_completestep_test.go index 9355020f2..158ca8f9a 100644 --- a/pkg/lifecycle/daemon/routes_navcycle_completestep_test.go +++ b/pkg/lifecycle/daemon/routes_navcycle_completestep_test.go @@ -156,7 +156,7 @@ func TestV2CompleteStep(t *testing.T) { POST: "/api/v1/navcycle/step/make-the-things", // need to wait until the async task completes before we check all the expected mock calls, // otherwise the state won't have been saved yet - WaitForCleanup: func() <-chan time.Time { return time.After(60 * time.Millisecond) }, + WaitForCleanup: func() <-chan time.Time { return time.After(120 * time.Millisecond) }, OnExecute: func(d *NavcycleRoutes, step api.Step) error { d.StepProgress.Store("make-the-things", daemontypes.StringProgress("unittest", "workin on it")) time.Sleep(60 * time.Millisecond) diff --git a/pkg/lifecycle/daemon/routes_navcycle_getstep.go b/pkg/lifecycle/daemon/routes_navcycle_getstep.go index 458130a02..e78d9f41e 100644 --- a/pkg/lifecycle/daemon/routes_navcycle_getstep.go +++ b/pkg/lifecycle/daemon/routes_navcycle_getstep.go @@ -87,7 +87,7 @@ func (d *NavcycleRoutes) hydrateStep(step daemontypes.Step) (*daemontypes.StepRe if helmValues != "" { step.HelmValues.Values = helmValues } else { - valuesFileContents, err := d.Fs.ReadFile(path.Join(constants.KustomizeHelmPath, "values.yaml")) + valuesFileContents, err := d.Fs.ReadFile(path.Join(constants.HelmChartPath, "values.yaml")) if err != nil { return nil, errors.Wrap(err, "read file values.yaml") } diff --git a/pkg/lifecycle/daemon/routes_v1.go b/pkg/lifecycle/daemon/routes_v1.go index c4b1c8743..0bef9f843 100644 --- a/pkg/lifecycle/daemon/routes_v1.go +++ b/pkg/lifecycle/daemon/routes_v1.go @@ -101,7 +101,7 @@ func (d *V1Routes) getHelmMetadata(release *api.Release) gin.HandlerFunc { debug.Log("event", "response.metadata") return func(c *gin.Context) { c.JSON(200, map[string]interface{}{ - "metadata": release.Metadata.HelmChartMetadata, + "metadata": release.Metadata.ShipAppMetadata, }) } } @@ -120,7 +120,7 @@ func (d *V1Routes) saveHelmValues(c *gin.Context) { } debug.Log("event", "validate") - linter := support.Linter{ChartDir: constants.KustomizeHelmPath} + linter := support.Linter{ChartDir: constants.HelmChartPath} rules.Templates(&linter, []byte(request.Values), "", false) if len(linter.Messages) > 0 { @@ -139,7 +139,7 @@ func (d *V1Routes) saveHelmValues(c *gin.Context) { return } - chartDefaultValues, err := d.Fs.ReadFile(path.Join(constants.KustomizeHelmPath, "values.yaml")) + chartDefaultValues, err := d.Fs.ReadFile(path.Join(constants.HelmChartPath, "values.yaml")) if err != nil { level.Error(d.Logger).Log("event", "values.readDefault.fail") diff --git a/pkg/lifecycle/helmValues/helmValues.go b/pkg/lifecycle/helmValues/helmValues.go index 92b6978cc..689869b63 100644 --- a/pkg/lifecycle/helmValues/helmValues.go +++ b/pkg/lifecycle/helmValues/helmValues.go @@ -55,7 +55,7 @@ func (h *helmValues) Execute(ctx context.Context, release *api.Release, step *ap daemonExitedChan := h.Daemon.EnsureStarted(ctx, release) - debug.Log("event", "readfile.attempt", "dest", path.Join(constants.KustomizeHelmPath, "values.yaml")) + debug.Log("event", "readfile.attempt", "dest", path.Join(constants.HelmChartPath, "values.yaml")) currentState, err := h.StateManager.TryLoad() if err != nil { @@ -109,7 +109,7 @@ func resolveStateHelmValues(logger log.Logger, manager state.Manager, fs afero.A } helmValues := editState.CurrentHelmValues() if helmValues == "" { - path := filepath.Join(constants.KustomizeHelmPath, "values.yaml") + path := filepath.Join(constants.HelmChartPath, "values.yaml") bytes, err := fs.ReadFile(path) if err != nil { return errors.Wrapf(err, "read helm values from %s", constants.TempHelmValuesPath) diff --git a/pkg/lifecycle/kustomize/kustomizer_test.go b/pkg/lifecycle/kustomize/kustomizer_test.go index 6772b5d2b..d24025d8d 100644 --- a/pkg/lifecycle/kustomize/kustomizer_test.go +++ b/pkg/lifecycle/kustomize/kustomizer_test.go @@ -102,7 +102,7 @@ func Test_kustomizer_writePatches(t *testing.T) { func Test_kustomizer_writeOverlay(t *testing.T) { mockStep := api.Kustomize{ - BasePath: constants.RenderedHelmPath, + BasePath: constants.KustomizeBasePath, Dest: path.Join("overlays", "ship"), } @@ -167,7 +167,7 @@ patches: func Test_kustomizer_writeBase(t *testing.T) { mockStep := api.Kustomize{ - BasePath: constants.RenderedHelmPath, + BasePath: constants.KustomizeBasePath, Dest: path.Join("overlays", "ship"), } @@ -185,7 +185,7 @@ func Test_kustomizer_writeBase(t *testing.T) { fields: fields{ GetFS: func() (afero.Afero, error) { fs := afero.Afero{Fs: afero.NewMemMapFs()} - err := fs.Mkdir(constants.RenderedHelmPath, 0777) + err := fs.Mkdir(constants.KustomizeBasePath, 0777) if err != nil { return afero.Afero{}, err } @@ -199,14 +199,14 @@ func Test_kustomizer_writeBase(t *testing.T) { fields: fields{ GetFS: func() (afero.Afero, error) { fs := afero.Afero{Fs: afero.NewMemMapFs()} - if err := fs.Mkdir(constants.RenderedHelmPath, 0777); err != nil { + if err := fs.Mkdir(constants.KustomizeBasePath, 0777); err != nil { return afero.Afero{}, err } files := []string{"a.yaml", "b.yaml", "c.yaml"} for _, file := range files { if err := fs.WriteFile( - path.Join(constants.RenderedHelmPath, file), + path.Join(constants.KustomizeBasePath, file), []byte{}, 0777, ); err != nil { @@ -229,7 +229,7 @@ func Test_kustomizer_writeBase(t *testing.T) { GetFS: func() (afero.Afero, error) { fs := afero.Afero{Fs: afero.NewMemMapFs()} nestedChartPath := path.Join( - constants.RenderedHelmPath, + constants.KustomizeBasePath, "charts/kube-stats-metrics/templates", ) if err := fs.MkdirAll(nestedChartPath, 0777); err != nil { @@ -243,7 +243,7 @@ func Test_kustomizer_writeBase(t *testing.T) { } for _, file := range files { if err := fs.WriteFile( - path.Join(constants.RenderedHelmPath, file), + path.Join(constants.KustomizeBasePath, file), []byte{}, 0777, ); err != nil { @@ -356,11 +356,11 @@ patches: mockState := state2.NewMockManager(mc) mockFS := afero.Afero{Fs: afero.NewMemMapFs()} - err := mockFS.Mkdir(constants.RenderedHelmPath, 0777) + err := mockFS.Mkdir(constants.KustomizeBasePath, 0777) req.NoError(err) err = mockFS.WriteFile( - path.Join(constants.RenderedHelmPath, "deployment.yaml"), + path.Join(constants.KustomizeBasePath, "deployment.yaml"), []byte{}, 0666, ) @@ -374,7 +374,7 @@ patches: mockDaemon.EXPECT().EnsureStarted(ctx, &release) mockDaemon.EXPECT().PushKustomizeStep(ctx, daemontypes.Kustomize{ - BasePath: constants.RenderedHelmPath, + BasePath: constants.KustomizeBasePath, }) mockDaemon.EXPECT().KustomizeSavedChan().Return(saveChan) mockState.EXPECT().TryLoad().Return(state.VersionedState{V1: &state.V1{ @@ -394,7 +394,7 @@ patches: ctx, &release, api.Kustomize{ - BasePath: constants.RenderedHelmPath, + BasePath: constants.KustomizeBasePath, Dest: "overlays/ship", }, ) diff --git a/pkg/lifecycle/render/backup.go b/pkg/lifecycle/render/backup.go index 5722bb66c..d46653668 100644 --- a/pkg/lifecycle/render/backup.go +++ b/pkg/lifecycle/render/backup.go @@ -1,37 +1,13 @@ package render import ( - "fmt" - - "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" - "github.com/pkg/errors" - "github.com/spf13/afero" + "github.com/replicatedhq/ship/pkg/util" ) func (r *noconfigrenderer) backupIfPresent(basePath string) error { - return backupIfPresent(r.Fs, basePath, r.Logger) + return util.BackupIfPresent(r.Fs, basePath, r.Logger, r.UI) } func (r *renderer) backupIfPresent(basePath string) error { - return backupIfPresent(r.Fs, basePath, r.Logger) -} - -func backupIfPresent(fs afero.Afero, basePath string, logger log.Logger) error { - exists, err := fs.Exists(basePath) - if err != nil { - return errors.Wrapf(err, "check file exists") - } - if !exists { - return nil - } - backupDest := fmt.Sprintf("%s.bak", basePath) - level.Info(logger).Log("step.type", "render", "event", "unpackTarget.backup.remove", "src", basePath, "dest", backupDest) - if err := fs.RemoveAll(backupDest); err != nil { - return errors.Wrapf(err, "backup existing dir %s to %s: remove existing %s", basePath, backupDest, backupDest) - } - if err := fs.Rename(basePath, backupDest); err != nil { - return errors.Wrapf(err, "backup existing dir %s to %s", basePath, backupDest) - } - return nil + return util.BackupIfPresent(r.Fs, basePath, r.Logger, r.UI) } diff --git a/pkg/lifecycle/render/dockerlayer/layer_test.go b/pkg/lifecycle/render/dockerlayer/layer_test.go index 77056b34b..7d8b68b36 100644 --- a/pkg/lifecycle/render/dockerlayer/layer_test.go +++ b/pkg/lifecycle/render/dockerlayer/layer_test.go @@ -82,8 +82,8 @@ func TestUnpackLayer(t *testing.T) { }) if test.dockerError == nil { - archiver.EXPECT().Open(&matchers.StartsWith{Value: "/tmp/dockerlayer"}, &matchers.StartsWith{Value: "/tmp/dockerlayer"}).Return(nil) - archiver.EXPECT().Open(&matchers.StartsWith{Value: "/tmp/dockerlayer"}, asset.Dest).Return(nil) + archiver.EXPECT().Open(&matchers.Contains{Value: "/dockerlayer"}, &matchers.Contains{Value: "/dockerlayer"}).Return(nil) + archiver.EXPECT().Open(&matchers.Contains{Value: "/dockerlayer"}, asset.Dest).Return(nil) } rootFs := root.Fs{ diff --git a/pkg/lifecycle/render/helm/commands.go b/pkg/lifecycle/render/helm/commands.go index 30f2b4f47..ec9b010f0 100644 --- a/pkg/lifecycle/render/helm/commands.go +++ b/pkg/lifecycle/render/helm/commands.go @@ -1,6 +1,11 @@ package helm import ( + "os" + + "fmt" + + "github.com/pkg/errors" "github.com/replicatedhq/ship/pkg/constants" "github.com/replicatedhq/ship/pkg/helm" ) @@ -10,15 +15,27 @@ type Commands interface { Init() error DependencyUpdate(chartRoot string) error Template(chartName string, args []string) error + Fetch(chartRef, repoURL, version, dest, home string) error } type helmCommands struct { - Home string +} + +func (h *helmCommands) Fetch(chartRef, repoURL, version, dest, home string) error { + outstring, err := helm.Fetch(chartRef, repoURL, version, dest, home) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("helm fetch failed, output %q", outstring)) + } + return nil } func (h *helmCommands) Init() error { - _, err := helm.Init(constants.TempHelmHomePath) - return err + err := os.MkdirAll(constants.InternalTempHelmHome, 0755) + if err != nil { + return errors.Wrapf(err, "create %s", constants.InternalTempHelmHome) + } + output, err := helm.Init(constants.InternalTempHelmHome) + return errors.Wrapf(err, "helm init: %s", output) } func (h *helmCommands) DependencyUpdate(chartRoot string) error { diff --git a/pkg/lifecycle/render/helm/fetch.go b/pkg/lifecycle/render/helm/fetch.go index dfaed0152..508fe7ef3 100644 --- a/pkg/lifecycle/render/helm/fetch.go +++ b/pkg/lifecycle/render/helm/fetch.go @@ -2,7 +2,6 @@ package helm import ( "context" - "fmt" "path" "github.com/go-kit/kit/log" @@ -11,7 +10,6 @@ import ( "github.com/replicatedhq/libyaml" "github.com/replicatedhq/ship/pkg/api" "github.com/replicatedhq/ship/pkg/constants" - "github.com/replicatedhq/ship/pkg/helm" "github.com/replicatedhq/ship/pkg/lifecycle/render/github" "github.com/replicatedhq/ship/pkg/lifecycle/render/root" "github.com/replicatedhq/ship/pkg/util" @@ -33,9 +31,10 @@ type ChartFetcher interface { // ClientFetcher is a ChartFetcher that does all the pulling/cloning client side type ClientFetcher struct { - Logger log.Logger - GitHub github.Renderer - FS afero.Afero + Logger log.Logger + GitHub github.Renderer + FS afero.Afero + HelmCommands Commands } func (f *ClientFetcher) FetchChart( @@ -76,15 +75,20 @@ func (f *ClientFetcher) FetchChart( return "", errors.Wrap(err, "get chart checkout tmpdir") } - outstring, err := helm.Init(constants.TempHelmHomePath) + err = f.HelmCommands.Init() if err != nil { - return "", errors.Wrap(err, fmt.Sprintf("helm init failed, output %q", outstring)) + return "", errors.Wrap(err, "init helm") } - outstring, err = helm.Fetch(asset.HelmFetch.ChartRef, asset.HelmFetch.RepoURL, asset.HelmFetch.Version, checkoutDir, constants.TempHelmHomePath) - + err = f.HelmCommands.Fetch( + asset.HelmFetch.ChartRef, + asset.HelmFetch.RepoURL, + asset.HelmFetch.Version, + checkoutDir, + constants.InternalTempHelmHome, + ) if err != nil { - return "", errors.Wrap(err, fmt.Sprintf("helm fetch failed, output %q", outstring)) + return "", errors.Wrap(err, "helm fetch") } // find the path that the chart was fetched to @@ -105,10 +109,12 @@ func NewFetcher( logger log.Logger, github github.Renderer, fs afero.Afero, + helmCommands Commands, ) ChartFetcher { return &ClientFetcher{ - Logger: logger, - GitHub: github, - FS: fs, + Logger: logger, + GitHub: github, + FS: fs, + HelmCommands: helmCommands, } } diff --git a/pkg/lifecycle/render/helm/template.go b/pkg/lifecycle/render/helm/template.go index eee9ffa4d..41c39deee 100644 --- a/pkg/lifecycle/render/helm/template.go +++ b/pkg/lifecycle/render/helm/template.go @@ -98,6 +98,7 @@ func (f *LocalTemplater) Template( templateArgs = append(templateArgs, args...) debug.Log("event", "helm.init") + // todo fix if err := f.Commands.Init(); err != nil { return errors.Wrap(err, "init helm client") } @@ -140,7 +141,7 @@ func (f *LocalTemplater) Template( } subChartsDirName := "charts" - tempRenderedChartDir := path.Join(constants.RenderedHelmTempPath, meta.HelmChartMetadata.Name) + tempRenderedChartDir := path.Join(constants.RenderedHelmTempPath, meta.ShipAppMetadata.Name) tempRenderedChartTemplatesDir := path.Join(tempRenderedChartDir, "templates") tempRenderedSubChartsDir := path.Join(tempRenderedChartDir, subChartsDirName) @@ -177,10 +178,10 @@ func (f *LocalTemplater) Template( func (f *LocalTemplater) tryRemoveRenderedHelmPath() error { debug := level.Debug(log.With(f.Logger, "method", "tryRemoveRenderedHelmPath")) - if err := f.FS.RemoveAll(constants.RenderedHelmPath); err != nil { + if err := f.FS.RemoveAll(constants.KustomizeBasePath); err != nil { return err } - debug.Log("event", "renderedHelmPath.remove", "path", constants.RenderedHelmPath) + debug.Log("event", "renderedHelmPath.remove", "path", constants.KustomizeBasePath) return nil } @@ -263,7 +264,7 @@ func writeStateHelmValuesToChartTmpdir(logger log.Logger, manager state.Manager, } helmValues := editState.CurrentHelmValues() if helmValues == "" { - defaultValuesShippedWithChart := filepath.Join(constants.KustomizeHelmPath, "values.yaml") + defaultValuesShippedWithChart := filepath.Join(constants.HelmChartPath, "values.yaml") bytes, err := fs.ReadFile(defaultValuesShippedWithChart) if err != nil { return errors.Wrapf(err, "read helm values from %s", defaultValuesShippedWithChart) diff --git a/pkg/lifecycle/render/helm/template_test.go b/pkg/lifecycle/render/helm/template_test.go index 841242d47..ad14a44a2 100644 --- a/pkg/lifecycle/render/helm/template_test.go +++ b/pkg/lifecycle/render/helm/template_test.go @@ -154,6 +154,9 @@ func TestForkTemplater(t *testing.T) { api.ReleaseMetadata{ Semver: "1.0.0", ChannelName: channelName, + ShipAppMetadata: api.ShipAppMetadata{ + Name: expectedChannelName, + }, }, []libyaml.ConfigGroup{}, test.templateContext, @@ -181,7 +184,7 @@ func TestTryRemoveRenderedHelmPath(t *testing.T) { { name: "base exists", describe: "ensure base is removed and removeAll doesn't error", - baseDir: constants.RenderedHelmPath, + baseDir: constants.KustomizeBasePath, expectError: false, }, { @@ -219,7 +222,7 @@ func TestTryRemoveRenderedHelmPath(t *testing.T) { if test.expectError { req.Error(removeErr) } else { - if dirExists, existErr := ft.FS.DirExists(constants.RenderedHelmPath); dirExists { + if dirExists, existErr := ft.FS.DirExists(constants.KustomizeBasePath); dirExists { req.NoError(existErr) // if dir exists, we expect tryRemoveRenderedHelmPath to have err'd req.Error(removeErr) diff --git a/pkg/lifecycle/render/render_test.go b/pkg/lifecycle/render/render_test.go index cc1cde6ad..fde8ac489 100644 --- a/pkg/lifecycle/render/render_test.go +++ b/pkg/lifecycle/render/render_test.go @@ -15,6 +15,7 @@ import ( "github.com/go-kit/kit/log" "github.com/golang/mock/gomock" + "github.com/mitchellh/cli" "github.com/replicatedhq/ship/pkg/api" "github.com/replicatedhq/ship/pkg/lifecycle/daemon/daemontypes" "github.com/replicatedhq/ship/pkg/lifecycle/render/planner" @@ -107,13 +108,13 @@ func TestBacksUpExisting(t *testing.T) { expect []string }{ { - name: "first", + name: "empty", target: "/tmp/installer", existing: []string{}, expect: []string{}, }, { - name: "first", + name: "backs up file", target: "/tmp/installer", existing: []string{ "/tmp/installer", @@ -134,6 +135,7 @@ func TestBacksUpExisting(t *testing.T) { Now: func() time.Time { return time.Unix(12345, 0) }, + UI: &cli.MockUi{}, } for _, filename := range test.existing { diff --git a/pkg/lifecycle/step.go b/pkg/lifecycle/step.go index 669c82608..79fe14172 100644 --- a/pkg/lifecycle/step.go +++ b/pkg/lifecycle/step.go @@ -46,7 +46,7 @@ func (s *StepExecutor) Execute(ctx context.Context, release *api.Release, step * err := s.Kustomizer.Execute(ctx, release, *step.Kustomize) debug.Log("event", "step.complete", "type", "kustomize", "err", err) return errors.Wrap(err, "execute kustomize step") - } else if step.HelmIntro != nil && release.Metadata.HelmChartMetadata.Readme != "" { + } else if step.HelmIntro != nil && release.Metadata.ShipAppMetadata.Readme != "" { debug.Log("event", "step.helmIntro", "type", "helmIntro") err := s.HelmIntro.Execute(ctx, release, step.HelmIntro) debug.Log("event", "step.complete", "type", "helmIntro", "err", err) diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index f8500da6f..44fe888a2 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -35,6 +35,7 @@ func FromViper(v *viper.Viper) log.Logger { globalLogger = withLevel(globalLogger, v.GetString("log-level")) globalLogger = log.With(globalLogger, "caller", fullPathCaller) golog.SetOutput(log.NewStdlibAdapter(level.Debug(globalLogger))) + return globalLogger } diff --git a/pkg/patch/patcher.go b/pkg/patch/patcher.go index 12c0e6f0c..fe4c63f71 100644 --- a/pkg/patch/patcher.go +++ b/pkg/patch/patcher.go @@ -206,7 +206,7 @@ func (p *ShipPatcher) ApplyPatch(patch string, step api.Kustomize, resource stri } debug.Log("event", "relPath") - relativePathToBases, err := filepath.Rel(constants.TempApplyOverlayPath, constants.RenderedHelmPath) + relativePathToBases, err := filepath.Rel(constants.TempApplyOverlayPath, constants.KustomizeBasePath) if err != nil { return nil, errors.Wrap(err, "failed to find relative path") } diff --git a/pkg/ship/dig.go b/pkg/ship/dig.go index ded9ca6f5..e520dee29 100644 --- a/pkg/ship/dig.go +++ b/pkg/ship/dig.go @@ -39,6 +39,9 @@ import ( "github.com/replicatedhq/ship/pkg/lifecycle/terraform/tfplan" "github.com/replicatedhq/ship/pkg/logger" "github.com/replicatedhq/ship/pkg/specs" + "github.com/replicatedhq/ship/pkg/specs/apptype" + "github.com/replicatedhq/ship/pkg/specs/githubclient" + "github.com/replicatedhq/ship/pkg/specs/replicatedapp" "github.com/replicatedhq/ship/pkg/state" "github.com/replicatedhq/ship/pkg/templates" "github.com/replicatedhq/ship/pkg/ui" @@ -60,6 +63,7 @@ func buildInjector(v *viper.Viper) (*dig.Container, error) { templates.NewBuilderBuilder, patch.NewShipPatcher, specs.NewIDPatcher, + apptype.NewInspector, daemon.NewV1Router, resolve.NewRenderer, @@ -69,8 +73,10 @@ func buildInjector(v *viper.Viper) (*dig.Container, error) { state.NewManager, planner.NewFactory, specs.NewResolver, - specs.NewGraphqlClient, - specs.NewGithubClient, lifecycle.NewRunner, + replicatedapp.NewGraphqlClient, + replicatedapp.NewAppResolver, + githubclient.NewGithubClient, + lifecycle.NewRunner, inline.NewRenderer, diff --git a/pkg/ship/kustomize.go b/pkg/ship/kustomize.go index 2d9692600..0259d1705 100644 --- a/pkg/ship/kustomize.go +++ b/pkg/ship/kustomize.go @@ -2,20 +2,21 @@ package ship import ( "context" - "path" "time" "strings" - "path/filepath" + "os" + + "fmt" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/pkg/errors" - "github.com/replicatedhq/libyaml" "github.com/replicatedhq/ship/pkg/api" "github.com/replicatedhq/ship/pkg/constants" "github.com/replicatedhq/ship/pkg/lifecycle/daemon/daemontypes" + "github.com/replicatedhq/ship/pkg/specs" "github.com/replicatedhq/ship/pkg/state" ) @@ -55,52 +56,34 @@ func (s *Ship) stateFileExists(ctx context.Context) bool { func (s *Ship) Update(ctx context.Context) error { debug := level.Debug(log.With(s.Logger, "method", "update")) + ctx, cancelFunc := context.WithCancel(ctx) + defer s.Shutdown(cancelFunc) + s.Daemon.SetProgress(daemontypes.StringProgress("kustomize", `loading state`)) // does a state already exist existingState, err := s.State.TryLoad() if err != nil { return errors.Wrap(err, "load state") } - s.Daemon.SetProgress(daemontypes.StringProgress("kustomize", `loading state`)) - if _, noExistingState := existingState.(state.Empty); noExistingState { debug.Log("event", "state.missing") return errors.New(`No state file found at ` + s.Viper.GetString("state-file") + `, please run "ship init"`) } - debug.Log("event", "read.chartURL") - helmChartPath := existingState.CurrentChartURL() - if helmChartPath == "" { - return errors.New(`No helm chart URL found at ` + s.Viper.GetString("state-file") + `, please run "ship init"`) + debug.Log("event", "read.upstream") + upstreamURL := existingState.Upstream() + if upstreamURL == "" { + return errors.New(fmt.Sprintf(`No upstream URL found at %s, please run "ship init"`, s.Viper.GetString("state-file"))) } debug.Log("event", "fetch latest chart") - s.Daemon.SetProgress(daemontypes.StringProgress("kustomize", `Downloading latest from upstream `+helmChartPath)) - helmChartMetadata, err := s.Resolver.ResolveChartMetadata(context.Background(), helmChartPath, existingState.CurrentChartRepoURL(), existingState.CurrentChartVersion()) - if err != nil { - return errors.Wrapf(err, "resolve helm chart metadata for %s", helmChartPath) - } + s.Daemon.SetProgress(daemontypes.StringProgress("kustomize", `Downloading latest from upstream `+upstreamURL)) - spec, err := s.Resolver.ResolveChartReleaseSpec(ctx) - if err != nil { - return errors.Wrapf(err, "resolve chart release for %s", filepath.Join(constants.KustomizeHelmPath, "ship.yaml")) - } - - debug.Log("event", "build helm release") - release := &api.Release{ - Metadata: api.ReleaseMetadata{ - HelmChartMetadata: helmChartMetadata, - }, - Spec: spec, - } + release, err := s.Resolver.ResolveRelease(ctx, upstreamURL) release.Spec.Lifecycle = s.IDPatcher.EnsureAllStepsHaveUniqueIDs(release.Spec.Lifecycle) - s.State.SerializeContentSHA(helmChartMetadata.ContentSHA) - - s.State.SerializeContentSHA(helmChartMetadata.ContentSHA) - return s.execute(ctx, release, nil, true) } @@ -120,8 +103,9 @@ func (s *Ship) Watch(ctx context.Context) error { return errors.New(`No state found, please run "ship init"`) } - debug.Log("event", "read.chartURL") - helmChartPath := existingState.CurrentChartURL() + debug.Log("event", "read.upstream") + + helmChartPath := existingState.Upstream() if helmChartPath == "" { return errors.New(`No current chart url found at ` + s.Viper.GetString("state-file") + `, please run "ship init"`) } @@ -133,12 +117,12 @@ func (s *Ship) Watch(ctx context.Context) error { } debug.Log("event", "fetch latest chart") - helmChartMetadata, err := s.Resolver.ResolveChartMetadata(context.Background(), helmChartPath, existingState.CurrentChartRepoURL(), existingState.CurrentChartVersion()) + release, err := s.Resolver.ResolveRelease(context.Background(), string(helmChartPath)) if err != nil { return errors.Wrapf(err, "resolve helm chart metadata for %s", helmChartPath) } - if helmChartMetadata.ContentSHA != existingState.Versioned().V1.ContentSHA { + if release.Metadata.ShipAppMetadata.ContentSHA != existingState.Versioned().V1.ContentSHA { debug.Log("event", "new sha") return nil } @@ -159,7 +143,7 @@ func (s *Ship) Init(ctx context.Context) error { } // does a state file exist on disk? - if s.stateFileExists(ctx) && !s.Viper.GetBool("rm-state") { + if s.stateFileExists(ctx) && os.Getenv("RM_STATE") != "" { debug.Log("event", "state.exists") useUpdate, err := s.UI.Ask(` @@ -193,50 +177,7 @@ Continuing will delete this state, would you like to continue? There is no undo. } func (s *Ship) fakeKustomizeRawRelease() *api.Release { - release := &api.Release{ - Spec: api.Spec{ - Assets: api.Assets{ - V1: []api.Asset{}, - }, - Config: api.Config{ - V1: []libyaml.ConfigGroup{}, - }, - Lifecycle: api.Lifecycle{ - V1: []api.Step{ - { - KustomizeIntro: &api.KustomizeIntro{ - StepShared: api.StepShared{ - ID: "kustomize-intro", - }, - }, - }, - { - Kustomize: &api.Kustomize{ - BasePath: s.KustomizeRaw, - Dest: path.Join("overlays", "ship"), - StepShared: api.StepShared{ - ID: "kustomize", - Invalidates: []string{"diff"}, - }, - }, - }, - { - Message: &api.Message{ - StepShared: api.StepShared{ - ID: "outro", - }, - Contents: ` -Assets are ready to deploy. You can run - - kustomize build overlays/ship | kubectl apply -f - - -to deploy the overlaid assets to your cluster. - `}, - }, - }, - }, - }, + return &api.Release{ + Spec: specs.DefaultRawRelease(s.KustomizeRaw), } - - return release } diff --git a/pkg/ship/ship.go b/pkg/ship/ship.go index 2aae62b5c..e14668405 100644 --- a/pkg/ship/ship.go +++ b/pkg/ship/ship.go @@ -16,11 +16,14 @@ import ( "github.com/mitchellh/cli" "github.com/pkg/errors" "github.com/replicatedhq/ship/pkg/api" + "github.com/replicatedhq/ship/pkg/constants" "github.com/replicatedhq/ship/pkg/lifecycle" "github.com/replicatedhq/ship/pkg/lifecycle/daemon/daemontypes" "github.com/replicatedhq/ship/pkg/specs" + "github.com/replicatedhq/ship/pkg/specs/replicatedapp" "github.com/replicatedhq/ship/pkg/state" "github.com/replicatedhq/ship/pkg/version" + "github.com/spf13/afero" "github.com/spf13/viper" ) @@ -38,13 +41,14 @@ type Ship struct { InstallationID string PlanOnly bool - Daemon daemontypes.Daemon - Resolver *specs.Resolver - Runbook string - Client *specs.GraphQLClient - UI cli.Ui - State state.Manager - IDPatcher *specs.IDPatcher + Daemon daemontypes.Daemon + Resolver *specs.Resolver + AppResolver replicatedapp.Resolver + Runbook string + UI cli.Ui + State state.Manager + IDPatcher *specs.IDPatcher + FS afero.Afero KustomizeRaw string Runner *lifecycle.Runner @@ -56,11 +60,12 @@ func NewShip( v *viper.Viper, daemon daemontypes.Daemon, resolver *specs.Resolver, - graphql *specs.GraphQLClient, + appresolver replicatedapp.Resolver, runner *lifecycle.Runner, ui cli.Ui, stateManager state.Manager, patcher *specs.IDPatcher, + fs afero.Afero, ) (*Ship, error) { return &Ship{ @@ -73,26 +78,28 @@ func NewShip( KustomizeRaw: v.GetString("raw"), - Viper: v, - Logger: logger, - Resolver: resolver, - Client: graphql, - Daemon: daemon, - UI: ui, - Runner: runner, - State: stateManager, - IDPatcher: patcher, + Viper: v, + Logger: logger, + Resolver: resolver, + AppResolver: appresolver, + Daemon: daemon, + UI: ui, + Runner: runner, + State: stateManager, + IDPatcher: patcher, + FS: fs, }, nil } func (s *Ship) Shutdown(cancelFunc context.CancelFunc) { + s.FS.RemoveAll(constants.ShipPathInternalTmp) // need to pause beforce canceling the context, because we need // the daemon to stay up for a few seconds so the UI can know its // time to show the "You're all done" page level.Info(s.Logger).Log("event", "shutdown.prePause", "waitTime", "1s") time.Sleep(1 * time.Second) - // now shut it all down, give things 5 seconds to clean up + // now shut it all down, give things 1 second to clean up level.Info(s.Logger).Log("event", "shutdown.commence", "waitTime", "1s") cancelFunc() time.Sleep(1 * time.Second) @@ -142,12 +149,12 @@ func (s *Ship) Execute(ctx context.Context) error { debug.Log("phase", "validate-inputs", "status", "complete") - selector := &specs.Selector{ + selector := &replicatedapp.Selector{ CustomerID: s.CustomerID, ReleaseSemver: s.ReleaseSemver, InstallationID: s.InstallationID, } - release, err := s.Resolver.ResolveAppRelease(ctx, selector) + release, err := s.AppResolver.ResolveAppRelease(ctx, selector) if err != nil { return errors.Wrap(err, "resolve specs") } @@ -156,7 +163,7 @@ func (s *Ship) Execute(ctx context.Context) error { return s.execute(ctx, release, selector, false) } -func (s *Ship) execute(ctx context.Context, release *api.Release, selector *specs.Selector, isKustomize bool) error { +func (s *Ship) execute(ctx context.Context, release *api.Release, selector *replicatedapp.Selector, isKustomize bool) error { runResultCh := make(chan error) go func() { defer close(runResultCh) @@ -180,7 +187,7 @@ func (s *Ship) execute(ctx context.Context, release *api.Release, selector *spec } if err == nil && !isKustomize && selector != nil { - _ = s.Resolver.RegisterInstall(ctx, *selector, release) + _ = s.AppResolver.RegisterInstall(ctx, *selector, release) } runResultCh <- err }() diff --git a/pkg/specs/apptype/determine_type.go b/pkg/specs/apptype/determine_type.go new file mode 100644 index 000000000..da7cee956 --- /dev/null +++ b/pkg/specs/apptype/determine_type.go @@ -0,0 +1,209 @@ +package apptype + +import ( + "context" + "path" + "strings" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/mitchellh/cli" + "github.com/pkg/errors" + "github.com/replicatedhq/ship/pkg/constants" + "github.com/replicatedhq/ship/pkg/helm" + helm2 "github.com/replicatedhq/ship/pkg/lifecycle/render/helm" + "github.com/replicatedhq/ship/pkg/specs/githubclient" + "github.com/replicatedhq/ship/pkg/state" + "github.com/replicatedhq/ship/pkg/util" + "github.com/spf13/afero" + "github.com/spf13/viper" +) + +type Inspector interface { + // DetermineApplicationType loads and application from upstream, + // returning the app type and the local path where its been downloaded (when applicable), + DetermineApplicationType( + ctx context.Context, + upstream string, + ) (appType string, localPath string, err error) +} + +func NewInspector( + logger log.Logger, + gh *githubclient.GithubClient, + fs afero.Afero, + v *viper.Viper, + stateManager state.Manager, + commands helm2.Commands, + ui cli.Ui, +) Inspector { + return &inspector{ + logger: logger, + github: gh, + fs: fs, + viper: v, + state: stateManager, + helm: commands, + ui: ui, + } +} + +type inspector struct { + logger log.Logger + github *githubclient.GithubClient + fs afero.Afero + viper *viper.Viper + state state.Manager + helm helm2.Commands + ui cli.Ui +} + +func (r *inspector) DetermineApplicationType( + ctx context.Context, + upstream string, +) (appType string, localPath string, err error) { + debug := level.Debug(log.With(r.logger, "method", "determineApplicationType")) + + // hack hack hack + isReplicatedApp := strings.HasPrefix(upstream, "replicated.app") || + strings.HasPrefix(upstream, "staging.replicated.app") || + strings.HasPrefix(upstream, "local.replicated.app") + if isReplicatedApp { + return "replicated.app", "", nil + } + + // "sure" + isGithub := strings.HasPrefix(strings.TrimLeft(upstream, "htps:/"), "github.com/") + + if isGithub { + return r.determineTypeFromGithubContents(ctx, upstream) + } + + // otherwise we're fetching the chart with `helm fetch` + chartRepoURL := r.viper.GetString("chart-repo-url") + chartVersion := r.viper.GetString("chart-version") + // persist helm options + err = r.state.SaveHelmOpts(chartRepoURL, chartVersion) + if err != nil { + return "", "", errors.Wrap(err, "write helm opts") + } + + debug.Log("event", "helm.init") + err = r.helm.Init() + if err != nil { + return "", "", errors.Wrapf(err, "helm init") + } + + debug.Log("event", "helm.fetch") + helmCmdOutput, err := r.fetchUnpackChartWithLibHelm( + upstream, + chartRepoURL, + chartVersion, + "chart", + constants.InternalTempHelmHome, + ) + if err != nil { + return "", "", errors.Wrapf(err, "fetch chart with helm: %s", helmCmdOutput) + } + return "helm", "chart", nil +} + +func (r *inspector) determineTypeFromGithubContents( + ctx context.Context, + upstream string, +) ( + applicationType string, + checkoutPath string, + err error, +) { + debug := level.Debug(r.logger) + savePath := path.Join(constants.ShipPathInternal, "tmp-repo") + err = r.github.GetRepoContent(ctx, upstream, savePath) + if err != nil { + return "", "", errors.Wrap(err, "fetch repo contents") + } + defer r.fs.RemoveAll(savePath) + // if there's a Chart.yaml, assume its a chart + isChart, err := r.fs.Exists(path.Join(savePath, "Chart.yaml")) + if err != nil { + return "", "", errors.Wrap(err, "check for Chart.yaml") + } + + if isChart { + destination := constants.HelmChartPath + err := util.BackupIfPresent(r.fs, destination, debug, r.ui) + if err != nil { + return "", "", errors.Wrapf(err, "try backup %s", destination) + } + err = r.fs.Rename(savePath, destination) + if err != nil { + return "", "", errors.Wrapf(err, "copy %s to chart/", savePath) + } + return "helm", destination, nil + } + + util.BackupIfPresent(r.fs, constants.KustomizeBasePath, debug, r.ui) + err = r.fs.Rename(savePath, constants.KustomizeBasePath) + if err != nil { + return "", "", errors.Wrapf(err, "copy %s to k8s/", savePath) + } + return "k8s", constants.KustomizeBasePath, nil +} + +// fetchUnpackChartWithLibHelm fetches and unpacks the chart into a temp directory, then copies the contents of the chart folder to +// the destination dir. +// TODO figure out how to copy files from host into afero filesystem for testing, or how to force helm to fetch into afero +func (r *inspector) fetchUnpackChartWithLibHelm( + chartRef, + repoURL, + version, + dest, + home string, +) (helmOutput string, err error) { + debug := level.Debug(log.With(r.logger, "method", "fetchUnpackChartWithLibHelm")) + + err = r.fs.MkdirAll(constants.ShipPathInternal, 0775) + if err != nil { + return "", errors.Wrap(err, "unable to create ship directory") + } + + tmpDest, err := r.fs.TempDir(constants.ShipPathInternal, "helm-fetch-unpack") + if err != nil { + return "", errors.Wrap(err, "unable to create temporary directory to unpack to") + } + defer r.fs.RemoveAll(tmpDest) + + // TODO: figure out how to get files into aferoFs here + helmOutput, err = helm.Fetch(chartRef, repoURL, version, tmpDest, home) + if err != nil { + return helmOutput, err + } + + subdir, err := util.FindOnlySubdir(tmpDest, r.fs) + if err != nil { + return "", errors.Wrap(err, "find chart subdir") + } + + // check if the destination directory exists - if it does, remove it + debug.Log("event", "checkExists", "path", dest) + saveDirExists, err := r.fs.Exists(dest) + if err != nil { + return "", errors.Wrapf(err, "check %s exists", dest) + } + + if saveDirExists { + debug.Log("event", "removeAll", "path", dest) + err := r.fs.RemoveAll(dest) + if err != nil { + return "", errors.Wrapf(err, "remove %s", dest) + } + } + + // rename that folder to move it to the destination directory + err = r.fs.Rename(subdir, dest) + if err != nil { + return "", errors.Wrapf(err, "rename %s to %s", subdir, dest) + } + + return "", nil +} diff --git a/pkg/specs/chart.go b/pkg/specs/chart.go index ff9bc221e..f34eca836 100644 --- a/pkg/specs/chart.go +++ b/pkg/specs/chart.go @@ -1,330 +1,267 @@ package specs import ( - "archive/tar" - "compress/gzip" "context" "crypto/sha256" "fmt" - "io" - "net/http" - "net/url" "os" "path" "path/filepath" - "strings" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" - "github.com/google/go-github/github" "github.com/pkg/errors" + "github.com/replicatedhq/libyaml" "github.com/replicatedhq/ship/pkg/api" "github.com/replicatedhq/ship/pkg/constants" - "github.com/replicatedhq/ship/pkg/helm" - "github.com/replicatedhq/ship/pkg/util" - "github.com/spf13/afero" "gopkg.in/yaml.v2" ) -type GithubClient struct { - client *github.Client - fs afero.Afero - logger log.Logger -} - -func NewGithubClient(fs afero.Afero, logger log.Logger) *GithubClient { - client := github.NewClient(nil) - return &GithubClient{ - client: client, - fs: fs, - logger: logger, - } -} - -var ( - DefaultHelmRelease = api.Release{ - Spec: api.Spec{ - Assets: api.Assets{ - V1: []api.Asset{ - { - Helm: &api.HelmAsset{ - AssetShared: api.AssetShared{ - Dest: constants.RenderedHelmPath, - }, - Local: &api.LocalHelmOpts{ - ChartRoot: constants.KustomizeHelmPath, - }, - HelmOpts: []string{ - "--values", - path.Join(constants.TempHelmValuesPath, "values.yaml"), - }, +func DefaultHelmRelease(chartPath string) api.Spec { + return api.Spec{ + Assets: api.Assets{ + V1: []api.Asset{ + { + Helm: &api.HelmAsset{ + AssetShared: api.AssetShared{ + Dest: constants.KustomizeBasePath, + }, + Local: &api.LocalHelmOpts{ + ChartRoot: chartPath, + }, + HelmOpts: []string{ + "--values", + path.Join(constants.TempHelmValuesPath, "values.yaml"), }, }, }, }, - Lifecycle: api.Lifecycle{ - V1: []api.Step{ - { - HelmIntro: &api.HelmIntro{ - StepShared: api.StepShared{ - ID: "intro", - }, + }, + Lifecycle: api.Lifecycle{ + V1: []api.Step{ + { + HelmIntro: &api.HelmIntro{ + StepShared: api.StepShared{ + ID: "intro", }, }, - { - HelmValues: &api.HelmValues{ - StepShared: api.StepShared{ - ID: "values", - Requires: []string{"intro"}, - Invalidates: []string{"render"}, - }, + }, + { + HelmValues: &api.HelmValues{ + StepShared: api.StepShared{ + ID: "values", + Requires: []string{"intro"}, + Invalidates: []string{"render"}, }, }, - { - Render: &api.Render{ - StepShared: api.StepShared{ - ID: "render", - Requires: []string{"values"}, - }, - Root: ".", + }, + { + Render: &api.Render{ + StepShared: api.StepShared{ + ID: "render", + Requires: []string{"values"}, }, + Root: ".", }, - { - KustomizeIntro: &api.KustomizeIntro{ - StepShared: api.StepShared{ - ID: "kustomize-intro", - }, + }, + { + KustomizeIntro: &api.KustomizeIntro{ + StepShared: api.StepShared{ + ID: "kustomize-intro", }, }, - { - Kustomize: &api.Kustomize{ - BasePath: constants.RenderedHelmPath, - Dest: path.Join("overlays", "ship"), - StepShared: api.StepShared{ - ID: "kustomize", - Requires: []string{"render"}, - }, + }, + { + Kustomize: &api.Kustomize{ + BasePath: constants.KustomizeBasePath, + Dest: path.Join("overlays", "ship"), + StepShared: api.StepShared{ + ID: "kustomize", + Requires: []string{"render"}, }, }, - { - Message: &api.Message{ - StepShared: api.StepShared{ - ID: "outro", - Requires: []string{"kustomize"}, - }, - Contents: ` + }, + { + Message: &api.Message{ + StepShared: api.StepShared{ + ID: "outro", + Requires: []string{"kustomize"}, + }, + Contents: ` Assets are ready to deploy. You can run kustomize build overlays/ship | kubectl apply -f - to deploy the overlaid assets to your cluster. `}, - }, }, }, }, } -) - -func (g *GithubClient) GetChartAndReadmeContents(ctx context.Context, chartURLString string) error { - debug := level.Debug(log.With(g.logger, "method", "getChartAndReadmeContents")) +} - if !strings.HasPrefix(chartURLString, "http") { - chartURLString = fmt.Sprintf("http://%s", chartURLString) - } +func DefaultRawRelease(basePath string) api.Spec { + return api.Spec{ + Assets: api.Assets{ + V1: []api.Asset{}, + }, + Config: api.Config{ + V1: []libyaml.ConfigGroup{}, + }, + Lifecycle: api.Lifecycle{ + V1: []api.Step{ + { + KustomizeIntro: &api.KustomizeIntro{ + StepShared: api.StepShared{ + ID: "kustomize-intro", + }, + }, + }, + { + Kustomize: &api.Kustomize{ + BasePath: basePath, + Dest: path.Join("overlays", "ship"), + StepShared: api.StepShared{ + ID: "kustomize", + Invalidates: []string{"diff"}, + }, + }, + }, + { + Message: &api.Message{ + StepShared: api.StepShared{ + ID: "outro", + }, + Contents: ` +Assets are ready to deploy. You can run - debug.Log("event", "parseURL") - chartURL, err := url.Parse(chartURLString) - if err != nil { - return err - } + kustomize build overlays/ship | kubectl apply -f - - if !strings.Contains(chartURL.Host, "github.com") { - return errors.New(fmt.Sprintf("%s is not a Github URL", chartURLString)) +to deploy the overlaid assets to your cluster. + `}, + }, + }, + }, } +} - owner, repo, branch, path, err := decodeGitHubURL(chartURL.Path) - if err != nil { - return err - } +func (r *Resolver) resolveMetadata(ctx context.Context, upstream, localPath string) (*api.ShipAppMetadata, error) { + debug := level.Debug(log.With(r.Logger, "method", "ResolveHelmMetadata")) - debug.Log("event", "checkExists", "path", constants.KustomizeHelmPath) - saveDirExists, err := g.fs.Exists(constants.KustomizeHelmPath) + baseMetadata, err := r.resolveBaseMetadata(upstream, localPath) if err != nil { - return errors.Wrap(err, "check kustomizeHelmPath exists") - } - - if saveDirExists { - debug.Log("event", "removeAll", "path", constants.KustomizeHelmPath) - err := g.fs.RemoveAll(constants.KustomizeHelmPath) - if err != nil { - return errors.Wrap(err, "remove kustomizeHelmPath") - } + return nil, errors.Wrap(err, "resolve base metadata") } - return g.downloadAndExtractFiles(ctx, owner, repo, branch, path, "/") -} - -func (g *GithubClient) downloadAndExtractFiles(ctx context.Context, owner string, repo string, branch string, basePath string, filePath string) error { - debug := level.Debug(log.With(g.logger, "method", "downloadAndExtractFiles")) - - debug.Log("event", "getContents", "path", basePath) + localChartPath := filepath.Join(localPath, "Chart.yaml") - archiveOpts := &github.RepositoryContentGetOptions{ - Ref: branch, - } - url, _, err := g.client.Repositories.GetArchiveLink(ctx, owner, repo, github.Tarball, archiveOpts) + exists, err := r.FS.Exists(localChartPath) if err != nil { - return errors.Wrapf(err, "get archive link for owner - %s repo - %s", owner, repo) + return nil, errors.Wrapf(err, "read file from %s", localChartPath) } - - resp, err := http.Get(url.String()) - if err != nil { - return errors.Wrapf(err, "downloading archive") + if !exists { + return baseMetadata, nil } - defer resp.Body.Close() - uncompressedStream, err := gzip.NewReader(resp.Body) + debug.Log("phase", "read-chart", "from", localChartPath) + chart, err := r.FS.ReadFile(localChartPath) if err != nil { - return errors.Wrapf(err, "create uncompressed stream") + return nil, errors.Wrapf(err, "read file from %s", localChartPath) } - tarReader := tar.NewReader(uncompressedStream) - - for { - header, err := tarReader.Next() - if err == io.EOF { - break - } - - if err != nil { - return errors.Wrapf(err, "extract tar gz, next()") - } - - switch header.Typeflag { - case tar.TypeDir: - dirName := strings.Join(strings.Split(header.Name, "/")[1:], "/") - if !strings.HasPrefix(dirName, basePath) { - continue - } - - dirName = strings.TrimPrefix(dirName, basePath) - if err := g.fs.MkdirAll(filepath.Join(constants.KustomizeHelmPath, filePath, dirName), 0755); err != nil { - return errors.Wrapf(err, "extract tar gz, mkdir") - } - case tar.TypeReg: - fileName := strings.Join(strings.Split(header.Name, "/")[1:], "/") - if !strings.HasPrefix(fileName, basePath) { - continue - } - fileName = strings.TrimPrefix(fileName, basePath) - outFile, err := g.fs.Create(filepath.Join(constants.KustomizeHelmPath, filePath, fileName)) - if err != nil { - return errors.Wrapf(err, "extract tar gz, create") - } - defer outFile.Close() - if _, err := io.Copy(outFile, tarReader); err != nil { - return errors.Wrapf(err, "extract tar gz, copy") - } - } + debug.Log("phase", "unmarshal-chart.yaml") + if err := yaml.Unmarshal(chart, &baseMetadata); err != nil { + return nil, err } - return nil + return baseMetadata, nil } -func (r *Resolver) ResolveChartMetadata(ctx context.Context, upstream, chartRepoURL, chartVersion string) (api.HelmChartMetadata, error) { - debug := level.Debug(log.With(r.Logger, "method", "ResolveChartMetadata", "upstream", upstream)) - - debug.Log("phase", "fetch-readme", "for", upstream) - var md api.HelmChartMetadata - if strings.Contains(upstream, "github.com") && chartRepoURL == "" && chartVersion == "" { - debug.Log("event", "fetch.withGitHub") - err := r.GithubClient.GetChartAndReadmeContents(ctx, upstream) - if err != nil { - return api.HelmChartMetadata{}, errors.Wrapf(err, "get chart and read me at %s", upstream) - } - } else { - debug.Log("event", "fetch.withHelm") - // fetch using 'helm fetch' - out, err := helm.Init(constants.TempHelmHomePath) - if err != nil { - return api.HelmChartMetadata{}, errors.Wrapf(err, "initialize helm to fetch chart: %s", out) - } - out, err = r.fetchUnpack(upstream, chartRepoURL, chartVersion, constants.KustomizeHelmPath, constants.TempHelmHomePath) - if err != nil { - return api.HelmChartMetadata{}, errors.Wrapf(err, "fetch chart with helm: %s", out) - } - } - - debug.Log("phase", "save-chart-url", "url", upstream) +// resolveBaseMetadata resolves URL, ContentSHA, and Readme for the resource +func (r *Resolver) resolveBaseMetadata(upstream string, localPath string) (*api.ShipAppMetadata, error) { + debug := level.Debug(log.With(r.Logger, "method", "resolveBaseMetaData")) + var md api.ShipAppMetadata md.URL = upstream - - debug.Log("phase", "calculate-sha", "for", constants.KustomizeHelmPath) - contentSHA, err := r.calculateContentSHA(constants.KustomizeHelmPath) + debug.Log("event", "upstream.Serialize", "for", localPath, "upstream", upstream) + err := r.StateManager.SerializeUpstream(upstream) + if err != nil { + return nil, errors.Wrapf(err, "write upstream") + } + debug.Log("phase", "calculate-sha", "for", localPath) + contentSHA, err := r.shaSummer(r, localPath) if err != nil { - return api.HelmChartMetadata{}, errors.Wrapf(err, "calculate chart sha") + return nil, errors.Wrapf(err, "calculate chart sha") } md.ContentSHA = contentSHA - localChartPath := filepath.Join(constants.KustomizeHelmPath, "Chart.yaml") - debug.Log("phase", "read-chart", "from", localChartPath) - chart, err := r.FS.ReadFile(localChartPath) + err = r.StateManager.SerializeContentSHA(contentSHA) if err != nil { - r.ui.Error( - "The input was not recognized as a supported asset type. Ship currently supports Helm, Knative, and Kubernetes applications. Check the URL and try again.\n", - ) - return api.HelmChartMetadata{}, errors.Wrapf(err, "read file from %s", localChartPath) + return nil, errors.Wrap(err, "write content sha") } - localReadmePath := filepath.Join(constants.KustomizeHelmPath, "README.md") + localReadmePath := filepath.Join(localPath, "README.md") debug.Log("phase", "read-readme", "from", localReadmePath) readme, err := r.FS.ReadFile(localReadmePath) if err != nil { if !os.IsNotExist(err) { - return api.HelmChartMetadata{}, errors.Wrapf(err, "read file from %s", localReadmePath) + return nil, errors.Wrapf(err, "read file from %s", localReadmePath) } } - - debug.Log("phase", "unmarshal-chart.yaml") - if err := yaml.Unmarshal(chart, &md); err != nil { - return api.HelmChartMetadata{}, err - } - if readme != nil { md.Readme = string(readme) - } - - return md, nil -} + } else { + // TODO default README better + md.Readme = fmt.Sprintf(` +Deployment Generator +=========================== -func (r *Resolver) ResolveChartReleaseSpec(ctx context.Context) (api.Spec, error) { - localReleasePath := filepath.Join(constants.KustomizeHelmPath, "ship.yaml") +This is a deployment generator for - r.ui.Info("Looking for ship.yaml ...") + ship init %s - if upstreamExists, err := r.FS.Exists(localReleasePath); err == nil && !upstreamExists { - r.ui.Info("ship.yaml not found ... Generating default ship.yaml for Helm application ...") - return DefaultHelmRelease.Spec, nil +Sources for your app have been generated at %s. This installer will walk you through +customizing these resources and preparing them to deploy to your infrastructure. +`, upstream, localPath) } + return &md, nil +} - upstreamRelease, err := r.FS.ReadFile(localReleasePath) - if err != nil { - level.Debug(r.Logger).Log("message", "failed to read upstream release, using default", "from", localReleasePath, "error", err) - r.ui.Info("Unable to read ship.yaml ... Generating default ship.yaml for Helm application ...") - return DefaultHelmRelease.Spec, nil +func (r *Resolver) maybeGetShipYAML(ctx context.Context, localPath string) (*api.Spec, error) { + localReleasePaths := []string{ + filepath.Join(localPath, "ship.yaml"), + filepath.Join(localPath, "ship.yml"), } - var spec api.Spec - if err := yaml.UnmarshalStrict(upstreamRelease, &spec); err != nil { - level.Debug(r.Logger).Log("event", "release.unmarshal.fail", "error", err) - return api.Spec{}, errors.Wrapf(err, "unmarshal ship.yaml") + r.ui.Info("Looking for ship.yaml ...") + + for _, shipYAMLPath := range localReleasePaths { + upstreamShipYAMLExists, err := r.FS.Exists(shipYAMLPath) + if err != nil { + return nil, errors.Wrapf(err, "check file %s exists", shipYAMLPath) + } + + if !upstreamShipYAMLExists { + continue + } + upstreamRelease, err := r.FS.ReadFile(shipYAMLPath) + if err != nil { + return nil, errors.Wrapf(err, "read file from %s", shipYAMLPath) + } + var spec api.Spec + if err := yaml.UnmarshalStrict(upstreamRelease, &spec); err != nil { + level.Debug(r.Logger).Log("event", "release.unmarshal.fail", "error", err) + return nil, errors.Wrapf(err, "unmarshal ship.yaml") + } + return &spec, nil } - return spec, nil + return nil, nil } +type shaSummer func(*Resolver, string) (string, error) + func (r *Resolver) calculateContentSHA(root string) (string, error) { - contents := []byte{} + var contents []byte err := r.FS.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return errors.Wrapf(err, "fs walk") @@ -349,75 +286,3 @@ func (r *Resolver) calculateContentSHA(root string) (string, error) { return fmt.Sprintf("%x", sha256.Sum256(contents)), nil } - -func decodeGitHubURL(chartPath string) (owner string, repo string, branch string, path string, err error) { - splitPath := strings.Split(chartPath, "/") - - if len(splitPath) < 3 { - return owner, repo, path, branch, errors.Wrapf(errors.New("unable to decode github url"), chartPath) - } - - owner = splitPath[1] - repo = splitPath[2] - branch = "" - path = "" - if len(splitPath) > 3 { - if splitPath[3] == "tree" { - branch = splitPath[4] - path = strings.Join(splitPath[5:], "/") - } else { - path = strings.Join(splitPath[3:], "/") - } - } - - return owner, repo, branch, path, nil -} - -// FetchUnpack fetches and unpacks the chart into a temp directory, then copies the contents of the chart folder to -// the destination dir. -// TODO figure out how to copy files from host into afero filesystem for testing, or how to force helm to fetch into afero -func (r *Resolver) fetchUnpack(chartRef, repoURL, version, dest, home string) (string, error) { - debug := level.Debug(log.With(r.Logger, "method", "fetchUnpack")) - - err := r.FS.MkdirAll(constants.ShipPath, 0775) - if err != nil { - return "", errors.Wrap(err, "unable to create ship directory") - } - - tmpDest, err := r.FS.TempDir(constants.ShipPath, "helm-fetch-unpack") - if err != nil { - return "", errors.Wrap(err, "unable to create temporary directory to unpack to") - } - defer r.FS.RemoveAll(tmpDest) - - // TODO: figure out how to get files into aferoFs here - out, err := helm.Fetch(chartRef, repoURL, version, tmpDest, home) - if err != nil { - return out, err - } - - subdir, err := util.FindOnlySubdir(tmpDest, r.FS) - if err != nil { - return "", errors.Wrap(err, "find chart subdir") - } - - // check if the destination directory exists - if it does, remove it - debug.Log("event", "checkExists", "path", constants.KustomizeHelmPath) - saveDirExists, err := r.FS.Exists(dest) - if err != nil { - return "", errors.Wrapf(err, "check %s exists", dest) - } - - if saveDirExists { - debug.Log("event", "removeAll", "path", constants.KustomizeHelmPath) - err := r.FS.RemoveAll(dest) - if err != nil { - return "", errors.Wrapf(err, "remove %s", dest) - } - } - - // rename that folder to move it to the destination directory - err = r.FS.Rename(subdir, dest) - - return "", err -} diff --git a/pkg/specs/chart_test.go b/pkg/specs/chart_test.go index 58302d86b..f2c009d28 100644 --- a/pkg/specs/chart_test.go +++ b/pkg/specs/chart_test.go @@ -2,13 +2,6 @@ package specs import ( "context" - "encoding/base64" - "io" - "net/http" - "net/http/httptest" - "net/url" - "path" - "strings" "testing" "github.com/go-kit/kit/log" @@ -18,7 +11,6 @@ import ( "path/filepath" - "github.com/google/go-github/github" "github.com/mitchellh/cli" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -29,175 +21,19 @@ import ( "github.com/stretchr/testify/require" ) -var client *github.Client -var mux *http.ServeMux -var serverURL string -var teardown func() - type ApplyUpstreamReleaseSpec struct { Name string Description string UpstreamShipYAML string - ExpectedSpec api.Spec -} - -func setupGitClient() (client *github.Client, mux *http.ServeMux, serveURL string, teardown func()) { - mux = http.NewServeMux() - server := httptest.NewServer(mux) - client = github.NewClient(nil) - url, _ := url.Parse(server.URL + "/") - client.BaseURL = url - client.UploadURL = url - - return client, mux, server.URL, server.Close + ExpectedSpec *api.Spec } -func TestGithubClient(t *testing.T) { +func TestSpecsResolver(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "GithubClient") + RunSpecs(t, "specsResolver") } -var _ = Describe("GithubClient", func() { - client, mux, serverURL, teardown = setupGitClient() - mux.HandleFunc("/repos/o/r/tarball", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, serverURL+"/archive.tar.gz", http.StatusFound) - return - }) - mux.HandleFunc("/archive.tar.gz", func(w http.ResponseWriter, r *http.Request) { - archiveData := `H4sIAJKjXFsAA+3WXW6CQBQFYJbCBmrv/D831ce+uIOpDtGEKQaoibt3qERbEmiNI6TxfC8TIwkXTg65lfW73D3ZcrXZ7t1zcg9EZJRKv059OonL09lKmRDcMM6k0SkxSYolqbrLNB2fVW3LMIoPr2DounBZlg383z7H+fwnqp/5v25sWc8O1ucR7xHeh5ZyKH9xzl+TDPkroylJKeIMvR48//fw8PC4Ov1fLl7mb4uZX8e8xzX9V4Y1/RdMof9jyIpi6hFgQp3+1y78tLWrYm6CV+1/oum/JqGx/42hN/+12+XFwbuPsA7euA3++v1n/LL/sZA/JyM4vv9juMQ89SQwhd7+V67cb1fu5vInf9n/zLf+y6b/nDP0fwxtzFOPAQAAAAAAAAAAAACRHQEZehxJACgAAA==` - dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(archiveData)) - w.Header().Set("Content-Type", "application/gzip") - io.Copy(w, dec) - }) - - Describe("GetChartAndReadmeContents", func() { - Context("With a url prefixed with http(s)", func() { - It("should fetch and persist README.md and Chart.yaml", func() { - validGitURLWithPrefix := "http://www.github.com/o/r/" - mockFs := afero.Afero{Fs: afero.NewMemMapFs()} - gitClient := GithubClient{ - client: client, - fs: mockFs, - logger: log.NewNopLogger(), - } - - err := gitClient.GetChartAndReadmeContents(context.Background(), validGitURLWithPrefix) - Expect(err).NotTo(HaveOccurred()) - - readme, err := gitClient.fs.ReadFile(path.Join(constants.KustomizeHelmPath, "README.md")) - Expect(err).NotTo(HaveOccurred()) - chart, err := gitClient.fs.ReadFile(path.Join(constants.KustomizeHelmPath, "Chart.yaml")) - Expect(err).NotTo(HaveOccurred()) - deployment, err := gitClient.fs.ReadFile(path.Join(constants.KustomizeHelmPath, "templates", "deployment.yml")) - Expect(err).NotTo(HaveOccurred()) - service, err := gitClient.fs.ReadFile(path.Join(constants.KustomizeHelmPath, "templates", "service.yml")) - Expect(err).NotTo(HaveOccurred()) - - Expect(string(readme)).To(Equal("foo")) - Expect(string(chart)).To(Equal("bar")) - Expect(string(deployment)).To(Equal("deployment")) - Expect(string(service)).To(Equal("service")) - }) - }) - - Context("With a url not prefixed with http", func() { - It("should fetch and persist README.md and Chart.yaml", func() { - validGitURLWithoutPrefix := "github.com/o/r" - mockFs := afero.Afero{Fs: afero.NewMemMapFs()} - gitClient := GithubClient{ - client: client, - fs: mockFs, - logger: log.NewNopLogger(), - } - - err := gitClient.GetChartAndReadmeContents(context.Background(), validGitURLWithoutPrefix) - Expect(err).NotTo(HaveOccurred()) - - readme, err := gitClient.fs.ReadFile(path.Join(constants.KustomizeHelmPath, "README.md")) - Expect(err).NotTo(HaveOccurred()) - chart, err := gitClient.fs.ReadFile(path.Join(constants.KustomizeHelmPath, "Chart.yaml")) - Expect(err).NotTo(HaveOccurred()) - deployment, err := gitClient.fs.ReadFile(path.Join(constants.KustomizeHelmPath, "templates", "deployment.yml")) - Expect(err).NotTo(HaveOccurred()) - service, err := gitClient.fs.ReadFile(path.Join(constants.KustomizeHelmPath, "templates", "service.yml")) - Expect(err).NotTo(HaveOccurred()) - - Expect(string(readme)).To(Equal("foo")) - Expect(string(chart)).To(Equal("bar")) - Expect(string(deployment)).To(Equal("deployment")) - Expect(string(service)).To(Equal("service")) - }) - }) - - Context("With a non-github url", func() { - It("should return an error", func() { - nonGithubURL := "gitlab.com/o/r" - mockFs := afero.Afero{Fs: afero.NewMemMapFs()} - gitClient := GithubClient{ - client: client, - fs: mockFs, - logger: log.NewNopLogger(), - } - - err := gitClient.GetChartAndReadmeContents(context.Background(), nonGithubURL) - Expect(err).NotTo(BeNil()) - Expect(err.Error()).To(Equal("http://gitlab.com/o/r is not a Github URL")) - }) - }) - }) - - Describe("decodeGitHubURL", func() { - Context("With a valid github url", func() { - It("should decode a valid url without a path", func() { - chartPath := "github.com/o/r" - o, r, b, p, err := decodeGitHubURL(chartPath) - Expect(err).NotTo(HaveOccurred()) - - Expect(o).To(Equal("o")) - Expect(r).To(Equal("r")) - Expect(p).To(Equal("")) - Expect(b).To(Equal("")) - }) - - It("should decode a valid url with a path", func() { - chartPath := "github.com/o/r/stable/chart" - o, r, b, p, err := decodeGitHubURL(chartPath) - Expect(err).NotTo(HaveOccurred()) - - Expect(o).To(Equal("o")) - Expect(r).To(Equal("r")) - Expect(p).To(Equal("stable/chart")) - Expect(b).To(Equal("")) - }) - - It("should decode a valid url with a /tree// path", func() { - chartPath := "github.com/o/r/tree/master/stable/chart" - o, r, b, p, err := decodeGitHubURL(chartPath) - Expect(err).NotTo(HaveOccurred()) - - Expect(o).To(Equal("o")) - Expect(r).To(Equal("r")) - Expect(p).To(Equal("stable/chart")) - Expect(b).To(Equal("master")) - }) - }) - - Context("With an invalid github url", func() { - It("should failed to decode a url without a path", func() { - chartPath := "github.com" - _, _, _, _, err := decodeGitHubURL(chartPath) - Expect(err).NotTo(BeNil()) - Expect(err.Error()).To(Equal("github.com: unable to decode github url")) - }) - - It("should fail to decode a url with a path", func() { - chartPath := "github.com/o" - _, _, _, _, err := decodeGitHubURL(chartPath) - Expect(err).NotTo(BeNil()) - Expect(err.Error()).To(Equal("github.com/o: unable to decode github url")) - }) - }) - }) +var _ = Describe("specs.Resolver", func() { Describe("calculateContentSHA", func() { Context("With multiple files", func() { @@ -227,16 +63,12 @@ var _ = Describe("GithubClient", func() { }) }) -var _ = AfterSuite(func() { - teardown() -}) - -func TestResolveChartRelease(t *testing.T) { +func TestMaybeGetShipYAML(t *testing.T) { tests := []ApplyUpstreamReleaseSpec{ { Name: "no upstream", Description: "no upstream, should use default release spec", - ExpectedSpec: DefaultHelmRelease.Spec, + ExpectedSpec: nil, }, { Name: "upstream exists", @@ -250,7 +82,7 @@ lifecycle: v1: - helmIntro: {} `, - ExpectedSpec: api.Spec{ + ExpectedSpec: &api.Spec{ Assets: api.Assets{ V1: []api.Asset{}, }, @@ -274,7 +106,7 @@ lifecycle: mockFs := afero.Afero{Fs: afero.NewMemMapFs()} if test.UpstreamShipYAML != "" { - mockFs.WriteFile(filepath.Join(constants.KustomizeHelmPath, "ship.yaml"), []byte(test.UpstreamShipYAML), 0755) + mockFs.WriteFile(filepath.Join(constants.HelmChartPath, "ship.yaml"), []byte(test.UpstreamShipYAML), 0755) } r := Resolver{ @@ -284,7 +116,7 @@ lifecycle: } ctx := context.Background() - spec, err := r.ResolveChartReleaseSpec(ctx) + spec, err := r.maybeGetShipYAML(ctx, constants.HelmChartPath) req.NoError(err) req.Equal(test.ExpectedSpec, spec) diff --git a/pkg/specs/constructor.go b/pkg/specs/constructor.go new file mode 100644 index 000000000..85d7b4394 --- /dev/null +++ b/pkg/specs/constructor.go @@ -0,0 +1,57 @@ +package specs + +import ( + "github.com/go-kit/kit/log" + "github.com/mitchellh/cli" + "github.com/replicatedhq/ship/pkg/specs/apptype" + "github.com/replicatedhq/ship/pkg/specs/githubclient" + "github.com/replicatedhq/ship/pkg/specs/replicatedapp" + "github.com/replicatedhq/ship/pkg/state" + "github.com/spf13/afero" + "github.com/spf13/viper" +) + +// A Resolver resolves specs +type Resolver struct { + Logger log.Logger + Client *replicatedapp.GraphQLClient + GithubClient *githubclient.GithubClient + StateManager state.Manager + FS afero.Afero + AppResolver replicatedapp.Resolver + + ui cli.Ui + appTypeInspector apptype.Inspector + shaSummer shaSummer + + Viper *viper.Viper +} + +// NewResolver builds a resolver from a Viper instance + +func NewResolver( + v *viper.Viper, + logger log.Logger, + fs afero.Afero, + graphql *replicatedapp.GraphQLClient, + githubClient *githubclient.GithubClient, + stateManager state.Manager, + ui cli.Ui, + determiner apptype.Inspector, + appresolver replicatedapp.Resolver, +) *Resolver { + return &Resolver{ + Logger: logger, + Client: graphql, + GithubClient: githubClient, + StateManager: stateManager, + FS: fs, + Viper: v, + ui: ui, + appTypeInspector: determiner, + shaSummer: func(resolver *Resolver, s string) (string, error) { + return resolver.calculateContentSHA(s) + }, + AppResolver: appresolver, + } +} diff --git a/pkg/specs/githubclient/client.go b/pkg/specs/githubclient/client.go new file mode 100644 index 000000000..e7cfe85e7 --- /dev/null +++ b/pkg/specs/githubclient/client.go @@ -0,0 +1,167 @@ +package githubclient + +import ( + "archive/tar" + "compress/gzip" + "context" + "fmt" + "io" + "net/http" + "net/url" + "path/filepath" + "strings" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/google/go-github/github" + "github.com/pkg/errors" + "github.com/spf13/afero" +) + +type GithubClient struct { + logger log.Logger + client *github.Client + fs afero.Afero +} + +func NewGithubClient(fs afero.Afero, logger log.Logger) *GithubClient { + client := github.NewClient(nil) + return &GithubClient{ + client: client, + fs: fs, + logger: logger, + } +} + +func (g *GithubClient) GetRepoContent( + ctx context.Context, + upstream string, + destinationPath string, +) error { + debug := level.Debug(log.With(g.logger, "method", "getRepoContents")) + + if !strings.HasPrefix(upstream, "http") { + + upstream = fmt.Sprintf("http://%s", upstream) + } + + debug.Log("event", "parseURL") + upstreamURL, err := url.Parse(upstream) + if err != nil { + return err + } + + if !strings.Contains(upstreamURL.Host, "github.com") { + return errors.New(fmt.Sprintf("%s is not a Github URL", upstream)) + } + + owner, repo, branch, repoPath, err := decodeGitHubURL(upstreamURL.Path) + if err != nil { + return err + } + + debug.Log("event", "removeAll", "destinationPath", destinationPath) + err = g.fs.RemoveAll(destinationPath) + if err != nil { + return errors.Wrap(err, "remove chart clone destination") + } + + return g.downloadAndExtractFiles(ctx, owner, repo, branch, repoPath, destinationPath) +} + +func (g *GithubClient) downloadAndExtractFiles( + ctx context.Context, + owner string, + repo string, + branch string, + basePath string, + filePath string, +) error { + debug := level.Debug(log.With(g.logger, "method", "downloadAndExtractFiles")) + + debug.Log("event", "getContents", "path", basePath) + + archiveOpts := &github.RepositoryContentGetOptions{ + Ref: branch, + } + url, _, err := g.client.Repositories.GetArchiveLink(ctx, owner, repo, github.Tarball, archiveOpts) + if err != nil { + return errors.Wrapf(err, "get archive link for owner - %s repo - %s", owner, repo) + } + + resp, err := http.Get(url.String()) + if err != nil { + return errors.Wrapf(err, "downloading archive") + } + defer resp.Body.Close() + + uncompressedStream, err := gzip.NewReader(resp.Body) + if err != nil { + return errors.Wrapf(err, "create uncompressed stream") + } + + tarReader := tar.NewReader(uncompressedStream) + + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + + if err != nil { + return errors.Wrapf(err, "extract tar gz, next()") + } + + switch header.Typeflag { + case tar.TypeDir: + dirName := strings.Join(strings.Split(header.Name, "/")[1:], "/") + if !strings.HasPrefix(dirName, basePath) { + continue + } + + dirName = strings.TrimPrefix(dirName, basePath) + if err := g.fs.MkdirAll(filepath.Join(filePath, dirName), 0755); err != nil { + return errors.Wrapf(err, "extract tar gz, mkdir") + } + case tar.TypeReg: + fileName := strings.Join(strings.Split(header.Name, "/")[1:], "/") + if !strings.HasPrefix(fileName, basePath) { + continue + } + fileName = strings.TrimPrefix(fileName, basePath) + outFile, err := g.fs.Create(filepath.Join(filePath, fileName)) + if err != nil { + return errors.Wrapf(err, "extract tar gz, create") + } + defer outFile.Close() + if _, err := io.Copy(outFile, tarReader); err != nil { + return errors.Wrapf(err, "extract tar gz, copy") + } + } + } + + return nil +} + +func decodeGitHubURL(chartPath string) (owner string, repo string, branch string, path string, err error) { + splitPath := strings.Split(chartPath, "/") + + if len(splitPath) < 3 { + return owner, repo, path, branch, errors.Wrapf(errors.New("unable to decode github url"), chartPath) + } + + owner = splitPath[1] + repo = splitPath[2] + branch = "" + path = "" + if len(splitPath) > 3 { + if splitPath[3] == "tree" { + branch = splitPath[4] + path = strings.Join(splitPath[5:], "/") + } else { + path = strings.Join(splitPath[3:], "/") + } + } + + return owner, repo, branch, path, nil +} diff --git a/pkg/specs/githubclient/client_test.go b/pkg/specs/githubclient/client_test.go new file mode 100644 index 000000000..a5465d163 --- /dev/null +++ b/pkg/specs/githubclient/client_test.go @@ -0,0 +1,189 @@ +package githubclient + +import ( + "context" + "encoding/base64" + "io" + "net/http" + "net/http/httptest" + "net/url" + "path" + "strings" + "testing" + + "github.com/go-kit/kit/log" + "github.com/google/go-github/github" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/replicatedhq/ship/pkg/constants" + "github.com/spf13/afero" +) + +var client *github.Client +var mux *http.ServeMux +var serverURL string +var teardown func() + +func setupGitClient() (client *github.Client, mux *http.ServeMux, serveURL string, teardown func()) { + mux = http.NewServeMux() + server := httptest.NewServer(mux) + client = github.NewClient(nil) + url, _ := url.Parse(server.URL + "/") + client.BaseURL = url + client.UploadURL = url + + return client, mux, server.URL, server.Close +} + +func TestGithubClient(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "GithubClient") +} + +var _ = Describe("GithubClient", func() { + client, mux, serverURL, teardown = setupGitClient() + mux.HandleFunc("/repos/o/r/tarball", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, serverURL+"/archive.tar.gz", http.StatusFound) + return + }) + mux.HandleFunc("/archive.tar.gz", func(w http.ResponseWriter, r *http.Request) { + archiveData := `H4sIAJKjXFsAA+3WXW6CQBQFYJbCBmrv/D831ce+uIOpDtGEKQaoibt3qERbEmiNI6TxfC8TIwkXTg65lfW73D3ZcrXZ7t1zcg9EZJRKv059OonL09lKmRDcMM6k0SkxSYolqbrLNB2fVW3LMIoPr2DounBZlg383z7H+fwnqp/5v25sWc8O1ucR7xHeh5ZyKH9xzl+TDPkroylJKeIMvR48//fw8PC4Ov1fLl7mb4uZX8e8xzX9V4Y1/RdMof9jyIpi6hFgQp3+1y78tLWrYm6CV+1/oum/JqGx/42hN/+12+XFwbuPsA7euA3++v1n/LL/sZA/JyM4vv9juMQ89SQwhd7+V67cb1fu5vInf9n/zLf+y6b/nDP0fwxtzFOPAQAAAAAAAAAAAACRHQEZehxJACgAAA==` + dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(archiveData)) + w.Header().Set("Content-Type", "application/gzip") + io.Copy(w, dec) + }) + + Describe("GetRepoContent", func() { + Context("With a url prefixed with http(s)", func() { + It("should fetch and persist README.md and Chart.yaml", func() { + validGitURLWithPrefix := "http://www.github.com/o/r/" + mockFs := afero.Afero{Fs: afero.NewMemMapFs()} + gitClient := &GithubClient{ + client: client, + fs: mockFs, + logger: log.NewNopLogger(), + } + + err := gitClient.GetRepoContent(context.Background(), validGitURLWithPrefix, constants.HelmChartPath) + Expect(err).NotTo(HaveOccurred()) + + readme, err := gitClient.fs.ReadFile(path.Join(constants.HelmChartPath, "README.md")) + Expect(err).NotTo(HaveOccurred()) + chart, err := gitClient.fs.ReadFile(path.Join(constants.HelmChartPath, "Chart.yaml")) + Expect(err).NotTo(HaveOccurred()) + deployment, err := gitClient.fs.ReadFile(path.Join(constants.HelmChartPath, "templates", "deployment.yml")) + Expect(err).NotTo(HaveOccurred()) + service, err := gitClient.fs.ReadFile(path.Join(constants.HelmChartPath, "templates", "service.yml")) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(readme)).To(Equal("foo")) + Expect(string(chart)).To(Equal("bar")) + Expect(string(deployment)).To(Equal("deployment")) + Expect(string(service)).To(Equal("service")) + }) + }) + + Context("With a url not prefixed with http", func() { + It("should fetch and persist README.md and Chart.yaml", func() { + validGitURLWithoutPrefix := "github.com/o/r" + mockFs := afero.Afero{Fs: afero.NewMemMapFs()} + gitClient := &GithubClient{ + client: client, + fs: mockFs, + logger: log.NewNopLogger(), + } + + err := gitClient.GetRepoContent(context.Background(), validGitURLWithoutPrefix, constants.HelmChartPath) + Expect(err).NotTo(HaveOccurred()) + + readme, err := gitClient.fs.ReadFile(path.Join(constants.HelmChartPath, "README.md")) + Expect(err).NotTo(HaveOccurred()) + chart, err := gitClient.fs.ReadFile(path.Join(constants.HelmChartPath, "Chart.yaml")) + Expect(err).NotTo(HaveOccurred()) + deployment, err := gitClient.fs.ReadFile(path.Join(constants.HelmChartPath, "templates", "deployment.yml")) + Expect(err).NotTo(HaveOccurred()) + service, err := gitClient.fs.ReadFile(path.Join(constants.HelmChartPath, "templates", "service.yml")) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(readme)).To(Equal("foo")) + Expect(string(chart)).To(Equal("bar")) + Expect(string(deployment)).To(Equal("deployment")) + Expect(string(service)).To(Equal("service")) + }) + }) + + Context("With a non-github url", func() { + It("should return an error", func() { + nonGithubURL := "gitlab.com/o/r" + mockFs := afero.Afero{Fs: afero.NewMemMapFs()} + gitClient := &GithubClient{ + client: client, + fs: mockFs, + logger: log.NewNopLogger(), + } + + err := gitClient.GetRepoContent(context.Background(), nonGithubURL, constants.HelmChartPath) + Expect(err).NotTo(BeNil()) + Expect(err.Error()).To(Equal("http://gitlab.com/o/r is not a Github URL")) + }) + }) + }) + + Describe("decodeGitHubURL", func() { + Context("With a valid github url", func() { + It("should decode a valid url without a path", func() { + chartPath := "github.com/o/r" + o, r, b, p, err := decodeGitHubURL(chartPath) + Expect(err).NotTo(HaveOccurred()) + + Expect(o).To(Equal("o")) + Expect(r).To(Equal("r")) + Expect(p).To(Equal("")) + Expect(b).To(Equal("")) + }) + + It("should decode a valid url with a path", func() { + chartPath := "github.com/o/r/stable/chart" + o, r, b, p, err := decodeGitHubURL(chartPath) + Expect(err).NotTo(HaveOccurred()) + + Expect(o).To(Equal("o")) + Expect(r).To(Equal("r")) + Expect(p).To(Equal("stable/chart")) + Expect(b).To(Equal("")) + }) + + It("should decode a valid url with a /tree// path", func() { + chartPath := "github.com/o/r/tree/master/stable/chart" + o, r, b, p, err := decodeGitHubURL(chartPath) + Expect(err).NotTo(HaveOccurred()) + + Expect(o).To(Equal("o")) + Expect(r).To(Equal("r")) + Expect(p).To(Equal("stable/chart")) + Expect(b).To(Equal("master")) + }) + }) + + Context("With an invalid github url", func() { + It("should failed to decode a url without a path", func() { + chartPath := "github.com" + _, _, _, _, err := decodeGitHubURL(chartPath) + Expect(err).NotTo(BeNil()) + Expect(err.Error()).To(Equal("github.com: unable to decode github url")) + }) + + It("should fail to decode a url with a path", func() { + chartPath := "github.com/o" + _, _, _, _, err := decodeGitHubURL(chartPath) + Expect(err).NotTo(BeNil()) + Expect(err.Error()).To(Equal("github.com/o: unable to decode github url")) + }) + }) + }) + +}) + +var _ = AfterSuite(func() { + teardown() +}) diff --git a/pkg/specs/interface.go b/pkg/specs/interface.go index b1301a305..19a4ecfb5 100644 --- a/pkg/specs/interface.go +++ b/pkg/specs/interface.go @@ -4,14 +4,12 @@ import ( "context" "fmt" "net/url" - "path/filepath" - "strings" "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" "github.com/pkg/errors" "github.com/replicatedhq/ship/pkg/api" - "github.com/replicatedhq/ship/pkg/constants" + "github.com/replicatedhq/ship/pkg/specs/replicatedapp" ) // A resolver turns a target string into a release. @@ -20,85 +18,69 @@ import ( // // github.com/helm/charts/stable/nginx-ingress // replicated.app/cool-ci-tool?customer_id=...&installation_id=... -// file:///home/bob/apps/ship.yml -// file:///home/luke/my-charts/deathstar_destroyer -func (r *Resolver) ResolveRelease(ctx context.Context, target string) (*api.Release, error) { +// file:///home/bob/apps/ship.yaml +// file:///home/luke/my-charts/proton-torpedoes +func (r *Resolver) ResolveRelease(ctx context.Context, upstream string) (*api.Release, error) { debug := log.With(level.Debug(r.Logger), "method", "ResolveRelease") - parsed, err := url.Parse(target) + parsed, err := url.Parse(upstream) if err != nil { - return nil, errors.Wrapf(err, "parse url %s", target) + return nil, errors.Wrapf(err, "parse url %s", upstream) } - r.ui.Info(fmt.Sprintf("Reading %s ...", target)) - // todo fetch it + r.ui.Info(fmt.Sprintf("Reading %s ...", upstream)) r.ui.Info("Determining application type ...") - applicationType := r.determineApplicationType(target) + applicationType, localPath, err := r.appTypeInspector.DetermineApplicationType(ctx, upstream) + if err != nil { + return nil, errors.Wrapf(err, "determine type of %s", upstream) + } debug.Log("event", "applicationType.resolve", "type", applicationType) r.ui.Info(fmt.Sprintf("Detected application type %s", applicationType)) switch applicationType { case "helm": - return r.resolveChart(ctx, target) + defaultRelease := DefaultHelmRelease(localPath) + return r.resolveRelease(ctx, upstream, localPath, &defaultRelease) + case "k8s": + defaultRelease := DefaultRawRelease(localPath) + return r.resolveRelease(ctx, upstream, localPath, &defaultRelease) case "replicated.app": - selector := (&Selector{}).unmarshalFrom(parsed) - return r.ResolveAppRelease(ctx, selector) + selector := (&replicatedapp.Selector{}).UnmarshalFrom(parsed) + return r.AppResolver.ResolveAppRelease(ctx, selector) } - return nil, errors.Errorf("unknown application type %q for target %q", applicationType, target) + return nil, errors.Errorf("unknown application type %q for upstream %q", applicationType, upstream) } -func (r *Resolver) resolveChart(ctx context.Context, target string) (*api.Release, error) { +func (r *Resolver) resolveRelease( + ctx context.Context, + upstream, + localPath string, + defaultSpec *api.Spec, +) (*api.Release, error) { debug := log.With(level.Debug(r.Logger), "method", "resolveChart") - chartRepoURL := r.Viper.GetString("chart-repo-url") - chartVersion := r.Viper.GetString("chart-version") - helmChartMetadata, err := r.ResolveChartMetadata(context.Background(), target, chartRepoURL, chartVersion) + metadata, err := r.resolveMetadata(context.Background(), upstream, localPath) if err != nil { - return nil, errors.Wrapf(err, "resolve helm metadata for %s", target) + return nil, errors.Wrapf(err, "resolve metadata for %s", localPath) } - // serialize the ChartURL to disk. First step in creating a state file - r.StateManager.SerializeChartURL(target) - - // persist helm options - err = r.StateManager.SaveHelmOpts(chartRepoURL, chartVersion) + debug.Log("event", "check upstream for ship.yaml") + spec, err := r.maybeGetShipYAML(ctx, localPath) if err != nil { - return nil, errors.Wrap(err, "write helm opts") + return nil, errors.Wrapf(err, "resolve ship.yaml release for %s", localPath) } - debug.Log("event", "check upstream release") - spec, err := r.ResolveChartReleaseSpec(ctx) - if err != nil { - return nil, errors.Wrapf(err, "resolve chart release for %s", filepath.Join(constants.KustomizeHelmPath, "ship.yaml")) - } - - debug.Log("event", "build helm release") - err = r.StateManager.SerializeContentSHA(helmChartMetadata.ContentSHA) - if err != nil { - return nil, errors.Wrap(err, "write content sha") + if spec == nil { + debug.Log("event", "no helm release") + r.ui.Info("ship.yaml not found in upstream, generating default lifecycle for application ...") + spec = defaultSpec } return &api.Release{ Metadata: api.ReleaseMetadata{ - HelmChartMetadata: helmChartMetadata, + ShipAppMetadata: *metadata, }, - Spec: spec, + Spec: *spec, }, nil } - -func (r *Resolver) determineApplicationType(target string) string { - // hack hack hack - isReplicatedApp := strings.HasPrefix(target, "replicated.app") || - strings.HasPrefix(target, "staging.replicated.app") || - strings.HasPrefix(target, "local.replicated.app") - - applicationType := "helm" - if isReplicatedApp { - applicationType = "replicated.app" - - } - - // todo more types - return applicationType -} diff --git a/pkg/specs/interface_test.go b/pkg/specs/interface_test.go new file mode 100644 index 000000000..3e88d56b9 --- /dev/null +++ b/pkg/specs/interface_test.go @@ -0,0 +1,201 @@ +package specs + +import ( + "context" + "testing" + + "path" + + "github.com/go-kit/kit/log" + "github.com/golang/mock/gomock" + "github.com/replicatedhq/ship/pkg/api" + replicatedapp2 "github.com/replicatedhq/ship/pkg/specs/replicatedapp" + "github.com/replicatedhq/ship/pkg/test-mocks/apptype" + "github.com/replicatedhq/ship/pkg/test-mocks/replicatedapp" + "github.com/replicatedhq/ship/pkg/test-mocks/state" + "github.com/replicatedhq/ship/pkg/test-mocks/ui" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" +) + +func TestResolver_ResolveRelease(t *testing.T) { + ctx := context.Background() + tests := []struct { + name string + upstream string + shaSummer shaSummer + expect func( + t *testing.T, + mockUi *ui.MockUi, + appType *apptype.MockInspector, + mockState *state.MockManager, + mockFs afero.Afero, + mockAppResolver *replicatedapp.MockResolver, + ) + expectRelease *api.Release + }{ + { + name: "helm chart in github", + upstream: "github.com/helm/charts/stable/x5", + shaSummer: func(resolver *Resolver, s string) (string, error) { + return "abcdef1234567890", nil + }, + expect: func( + t *testing.T, + mockUi *ui.MockUi, + appType *apptype.MockInspector, + mockState *state.MockManager, + mockFs afero.Afero, + mockAppResolver *replicatedapp.MockResolver, + ) { + req := require.New(t) + inOrder := mockUi.EXPECT().Info("Reading github.com/helm/charts/stable/x5 ...") + inOrder = mockUi.EXPECT().Info("Determining application type ...").After(inOrder) + inOrder = appType.EXPECT(). + DetermineApplicationType(ctx, "github.com/helm/charts/stable/x5"). + DoAndReturn(func(context.Context, string) (string, string, error) { + err := mockFs.MkdirAll("chart", 0755) + req.NoError(err) + err = mockFs.WriteFile(path.Join("chart", "README.md"), []byte("its the readme"), 0666) + req.NoError(err) + + err = mockFs.WriteFile(path.Join("chart", "Chart.yaml"), []byte(` +--- +version: 0.1.0 +name: i know what the x5 is +icon: https://kfbr.392/x5.png +`), 0666) + req.NoError(err) + return "helm", "chart", nil + }).After(inOrder) + inOrder = mockUi.EXPECT().Info("Detected application type helm").After(inOrder) + inOrder = mockState.EXPECT().SerializeUpstream("github.com/helm/charts/stable/x5").After(inOrder) + inOrder = mockState.EXPECT().SerializeContentSHA("abcdef1234567890").After(inOrder) + inOrder = mockUi.EXPECT().Info("Looking for ship.yaml ...").After(inOrder) + inOrder = mockUi.EXPECT().Info("ship.yaml not found in upstream, generating default lifecycle for application ...").After(inOrder) + + }, + expectRelease: &api.Release{ + Spec: DefaultHelmRelease("chart"), + Metadata: api.ReleaseMetadata{ + ShipAppMetadata: api.ShipAppMetadata{ + Version: "0.1.0", + URL: "github.com/helm/charts/stable/x5", + Readme: "its the readme", + Icon: "https://kfbr.392/x5.png", + Name: "i know what the x5 is", + ContentSHA: "abcdef1234567890", + }, + }, + }, + }, + { + name: "replicated.app", + upstream: "replicated.app?customer_id=12345&installation_id=67890", + expect: func( + t *testing.T, + mockUi *ui.MockUi, + appType *apptype.MockInspector, + mockState *state.MockManager, + mockFs afero.Afero, + mockAppResolver *replicatedapp.MockResolver, + ) { + inOrder := mockUi.EXPECT().Info("Reading replicated.app?customer_id=12345&installation_id=67890 ...") + inOrder = mockUi.EXPECT().Info("Determining application type ...").After(inOrder) + inOrder = appType.EXPECT(). + DetermineApplicationType(ctx, "replicated.app?customer_id=12345&installation_id=67890"). + DoAndReturn(func(context.Context, string) (string, string, error) { + return "replicated.app", "", nil + }).After(inOrder) + + inOrder = mockUi.EXPECT().Info("Detected application type replicated.app").After(inOrder) + inOrder = mockAppResolver.EXPECT().ResolveAppRelease(ctx, &replicatedapp2.Selector{ + CustomerID: "12345", + InstallationID: "67890", + }).Return(&api.Release{ + Metadata: api.ReleaseMetadata{ + ChannelName: "appgraph-coolci", + }, + }, nil).After(inOrder) + }, + expectRelease: &api.Release{ + Metadata: api.ReleaseMetadata{ + ChannelName: "appgraph-coolci", + }, + }, + }, + { + name: "plain k8s app", + upstream: "github.com/replicatedhq/test-charts/plain-k8s", + shaSummer: func(resolver *Resolver, s string) (string, error) { + return "abcdef1234567890", nil + }, + expect: func( + t *testing.T, + mockUi *ui.MockUi, + appType *apptype.MockInspector, + mockState *state.MockManager, + mockFs afero.Afero, + mockAppResolver *replicatedapp.MockResolver, + ) { + req := require.New(t) + inOrder := mockUi.EXPECT().Info("Reading github.com/replicatedhq/test-charts/plain-k8s ...") + inOrder = mockUi.EXPECT().Info("Determining application type ...").After(inOrder) + inOrder = appType.EXPECT(). + DetermineApplicationType(ctx, "github.com/replicatedhq/test-charts/plain-k8s"). + DoAndReturn(func(context.Context, string) (string, string, error) { + err := mockFs.MkdirAll("base", 0755) + req.NoError(err) + err = mockFs.WriteFile(path.Join("base", "README.md"), []byte("its the readme"), 0644) + req.NoError(err) + return "k8s", "base", nil + }).After(inOrder) + inOrder = mockUi.EXPECT().Info("Detected application type k8s").After(inOrder) + inOrder = mockState.EXPECT().SerializeUpstream("github.com/replicatedhq/test-charts/plain-k8s").After(inOrder) + inOrder = mockState.EXPECT().SerializeContentSHA("abcdef1234567890").After(inOrder) + inOrder = mockUi.EXPECT().Info("Looking for ship.yaml ...").After(inOrder) + inOrder = mockUi.EXPECT().Info("ship.yaml not found in upstream, generating default lifecycle for application ...").After(inOrder) + + }, + expectRelease: &api.Release{ + Spec: DefaultRawRelease("base"), + Metadata: api.ReleaseMetadata{ + ShipAppMetadata: api.ShipAppMetadata{ + URL: "github.com/replicatedhq/test-charts/plain-k8s", + Readme: "its the readme", + ContentSHA: "abcdef1234567890", + }, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := require.New(t) + mc := gomock.NewController(t) + mockUI := ui.NewMockUi(mc) + appType := apptype.NewMockInspector(mc) + mockState := state.NewMockManager(mc) + mockAppResolver := replicatedapp.NewMockResolver(mc) + mockFs := afero.Afero{Fs: afero.NewMemMapFs()} + resolver := &Resolver{ + Logger: log.NewNopLogger(), + StateManager: mockState, + FS: mockFs, + AppResolver: mockAppResolver, + ui: mockUI, + appTypeInspector: appType, + shaSummer: test.shaSummer, + } + test.expect(t, mockUI, appType, mockState, mockFs, mockAppResolver) + + func() { + defer mc.Finish() + release, err := resolver.ResolveRelease(ctx, test.upstream) + req.NoError(err) + req.Equal(test.expectRelease, release) + + }() + }) + } +} diff --git a/pkg/specs/patch.go b/pkg/specs/patch.go index f84ba19d4..f4705c4ab 100644 --- a/pkg/specs/patch.go +++ b/pkg/specs/patch.go @@ -21,21 +21,33 @@ type IDPatcher struct { type idSet map[string]interface{} +func (s idSet) contains(id string) bool { + _, ok := s[id] + return ok + +} + +func (s idSet) add(id string) { + s[id] = true +} + func (p *IDPatcher) EnsureAllStepsHaveUniqueIDs(lc api.Lifecycle) api.Lifecycle { newLc := api.Lifecycle{V1: []api.Step{}} seenIds := make(idSet) for _, step := range lc.V1 { id := step.Shared().ID - if id != "" && !p.contains(seenIds, id) { - seenIds[id] = true + if id != "" && !seenIds.contains(id) { + // the id is unique, add it to the ones we've seen, and append this step to the new lifecycle + seenIds.add(id) newLc.V1 = append(newLc.V1, step) continue } + // the current step's id is missing or a duplicate, so generate a new one newID := p.generateID(seenIds, step) level.Debug(p.Logger).Log("event", "id.generate", "id", newID) - seenIds[newID] = true + seenIds.add(newID) step.Shared().ID = newID newLc.V1 = append(newLc.V1, step) } @@ -45,7 +57,7 @@ func (p *IDPatcher) EnsureAllStepsHaveUniqueIDs(lc api.Lifecycle) api.Lifecycle func (p *IDPatcher) generateID(seenIds idSet, step api.Step) string { // try with the $shortname candidateID := fmt.Sprintf("%s", step.ShortName()) - if _, ok := seenIds[candidateID]; !ok { + if !seenIds.contains(candidateID) { return candidateID } @@ -53,7 +65,7 @@ func (p *IDPatcher) generateID(seenIds idSet, step api.Step) string { i := 2 for i < 100 { candidateID := fmt.Sprintf("%s-%d", step.ShortName(), i) - if _, ok := seenIds[candidateID]; !ok { + if !seenIds.contains(candidateID) { return candidateID } i++ @@ -66,8 +78,3 @@ func (p *IDPatcher) generateID(seenIds idSet, step api.Step) string { (&templates.StaticCtx{Logger: p.Logger}).RandomString(12), ) } - -func (p *IDPatcher) contains(seenIDs idSet, value string) bool { - _, ok := seenIDs[value] - return ok -} diff --git a/pkg/specs/graphql.go b/pkg/specs/replicatedapp/graphql.go similarity index 99% rename from pkg/specs/graphql.go rename to pkg/specs/replicatedapp/graphql.go index 669c6158a..2dfcd7981 100644 --- a/pkg/specs/graphql.go +++ b/pkg/specs/replicatedapp/graphql.go @@ -1,4 +1,4 @@ -package specs +package replicatedapp import ( "bytes" diff --git a/pkg/specs/resolver.go b/pkg/specs/replicatedapp/resolver.go similarity index 82% rename from pkg/specs/resolver.go rename to pkg/specs/replicatedapp/resolver.go index cfa48282b..a85ce9339 100644 --- a/pkg/specs/resolver.go +++ b/pkg/specs/replicatedapp/resolver.go @@ -1,4 +1,4 @@ -package specs +package replicatedapp import ( "context" @@ -7,61 +7,58 @@ import ( "github.com/go-kit/kit/log" "github.com/go-kit/kit/log/level" - "github.com/mitchellh/cli" "github.com/pkg/errors" "github.com/replicatedhq/ship/pkg/api" "github.com/replicatedhq/ship/pkg/constants" "github.com/replicatedhq/ship/pkg/helpers/flags" - "github.com/replicatedhq/ship/pkg/state" "github.com/spf13/afero" "github.com/spf13/viper" "gopkg.in/yaml.v2" ) -// A Resolver resolves specs -type Resolver struct { +type resolver struct { Logger log.Logger Client *GraphQLClient - GithubClient *GithubClient - StateManager state.Manager FS afero.Afero Runbook string SetChannelName string RunbookReleaseSemver string SetChannelIcon string - ui cli.Ui - - Viper *viper.Viper } -// NewResolver builds a resolver from a Viper instance -func NewResolver( +// NewAppResolver builds a resolver from a Viper instance +func NewAppResolver( v *viper.Viper, logger log.Logger, fs afero.Afero, graphql *GraphQLClient, - githubClient *GithubClient, - stateManager state.Manager, - ui cli.Ui, -) *Resolver { - return &Resolver{ +) Resolver { + return &resolver{ Logger: logger, Client: graphql, - GithubClient: githubClient, - StateManager: stateManager, FS: fs, Runbook: flags.GetCurrentOrDeprecatedString(v, "runbook", "studio-file"), SetChannelName: flags.GetCurrentOrDeprecatedString(v, "set-channel-name", "studio-channel-name"), SetChannelIcon: flags.GetCurrentOrDeprecatedString(v, "set-channel-icon", "studio-channel-icon"), RunbookReleaseSemver: v.GetString("release-semver"), - Viper: v, - ui: ui, } } +type Resolver interface { + ResolveAppRelease( + ctx context.Context, + selector *Selector, + ) (*api.Release, error) + RegisterInstall( + ctx context.Context, + selector Selector, + release *api.Release, + ) error +} + // ResolveAppRelease uses the passed config options to get specs from pg.replicated.com or // from a local runbook if so configured -func (r *Resolver) ResolveAppRelease(ctx context.Context, selector *Selector) (*api.Release, error) { +func (r *resolver) ResolveAppRelease(ctx context.Context, selector *Selector) (*api.Release, error) { var specYAML []byte var err error var release *ShipRelease @@ -97,7 +94,7 @@ func (r *Resolver) ResolveAppRelease(ctx context.Context, selector *Selector) (* return result, nil } -func (r *Resolver) resolveRunbookRelease() (*ShipRelease, error) { +func (r *resolver) resolveRunbookRelease() (*ShipRelease, error) { debug := level.Debug(log.With(r.Logger, "method", "resolveRunbookRelease")) debug.Log("phase", "load-specs", "from", "runbook", "file", r.Runbook) @@ -120,7 +117,7 @@ func (r *Resolver) resolveRunbookRelease() (*ShipRelease, error) { }, nil } -func (r *Resolver) resolveCloudRelease(selector *Selector) (*ShipRelease, error) { +func (r *resolver) resolveCloudRelease(selector *Selector) (*ShipRelease, error) { debug := level.Debug(log.With(r.Logger, "method", "resolveCloudSpec")) client := r.Client @@ -139,7 +136,7 @@ func (r *Resolver) resolveCloudRelease(selector *Selector) (*ShipRelease, error) } // persistSpec persists last-used YAML to disk at .ship/release.yml -func (r *Resolver) persistSpec(specYAML []byte) error { +func (r *resolver) persistSpec(specYAML []byte) error { if err := r.FS.MkdirAll(filepath.Dir(constants.ReleasePath), 0700); err != nil { return errors.Wrap(err, "mkdir yaml") } @@ -150,7 +147,7 @@ func (r *Resolver) persistSpec(specYAML []byte) error { return nil } -func (r *Resolver) RegisterInstall(ctx context.Context, selector Selector, release *api.Release) error { +func (r *resolver) RegisterInstall(ctx context.Context, selector Selector, release *api.Release) error { if r.Runbook != "" { return nil } diff --git a/pkg/specs/resolver_test.go b/pkg/specs/replicatedapp/resolver_test.go similarity index 63% rename from pkg/specs/resolver_test.go rename to pkg/specs/replicatedapp/resolver_test.go index ed505f8ad..b5d2cd7ad 100644 --- a/pkg/specs/resolver_test.go +++ b/pkg/specs/replicatedapp/resolver_test.go @@ -1,27 +1,18 @@ -package specs +package replicatedapp import ( "reflect" "testing" "github.com/replicatedhq/ship/pkg/constants" - - "github.com/go-kit/kit/log" - "github.com/replicatedhq/ship/pkg/state" "github.com/spf13/afero" - "github.com/spf13/viper" "github.com/stretchr/testify/require" ) func TestPersistSpec(t *testing.T) { - r := &Resolver{ + r := &resolver{ FS: afero.Afero{Fs: afero.NewMemMapFs()}, - StateManager: &state.MManager{ - Logger: log.NewNopLogger(), - FS: afero.Afero{Fs: afero.NewMemMapFs()}, - V: viper.New(), - }, } req := require.New(t) diff --git a/pkg/specs/selector.go b/pkg/specs/replicatedapp/selector.go similarity index 93% rename from pkg/specs/selector.go rename to pkg/specs/replicatedapp/selector.go index 52e30dde3..f4420133e 100644 --- a/pkg/specs/selector.go +++ b/pkg/specs/replicatedapp/selector.go @@ -1,4 +1,4 @@ -package specs +package replicatedapp import ( "net/url" @@ -30,7 +30,7 @@ func (s *Selector) String() string { } // this is kinda janky -func (s *Selector) unmarshalFrom(url *url.URL) *Selector { +func (s *Selector) UnmarshalFrom(url *url.URL) *Selector { for key, values := range url.Query() { switch key { case "customer_id": diff --git a/pkg/specs/selector_test.go b/pkg/specs/replicatedapp/selector_test.go similarity index 92% rename from pkg/specs/selector_test.go rename to pkg/specs/replicatedapp/selector_test.go index 997981f1a..a7bf32018 100644 --- a/pkg/specs/selector_test.go +++ b/pkg/specs/replicatedapp/selector_test.go @@ -1,4 +1,4 @@ -package specs +package replicatedapp import ( "net/url" @@ -39,7 +39,7 @@ func TestUnmarshalSelector(t *testing.T) { req := require.New(t) parsed, err := url.Parse(test.url) req.NoError(err) - actual := (&Selector{}).unmarshalFrom(parsed) + actual := (&Selector{}).UnmarshalFrom(parsed) req.Equal(test.want, actual) }) } diff --git a/pkg/state/manager.go b/pkg/state/manager.go index 5e77d51a7..a290786a7 100644 --- a/pkg/state/manager.go +++ b/pkg/state/manager.go @@ -29,7 +29,7 @@ type Manager interface { TryLoad() (State, error) RemoveStateFile() error SaveKustomize(kustomize *Kustomize) error - SerializeChartURL(URL string) error + SerializeUpstream(URL string) error SerializeContentSHA(contentSHA string) error SaveHelmOpts(url, version string) error Save(v VersionedState) error @@ -63,19 +63,20 @@ func NewManager( } } -// SerializeChartURL is used by `ship init` to serialize a state file with ChartURL to disk -func (m *MManager) SerializeChartURL(URL string) error { - debug := level.Debug(log.With(m.Logger, "method", "SerializeChartURL")) +// SerializeUpstream is used by `ship init` to serialize a state file with ChartURL to disk +func (m *MManager) SerializeUpstream(upstream string) error { + debug := level.Debug(log.With(m.Logger, "method", "SerializeUpstream")) - debug.Log("event", "tryLoadState") - currentState, err := m.TryLoad() + current, err := m.TryLoad() if err != nil { - return errors.Wrap(err, "try load state") + return errors.Wrap(err, "load state") } - versionedState := currentState.Versioned() - versionedState.V1.ChartURL = URL + debug.Log("event", "generateUpstreamURLState") - return m.serializeAndWriteState(versionedState) + toSerialize := current.Versioned() + toSerialize.V1.Upstream = upstream + + return m.serializeAndWriteState(toSerialize) } // SerializeContentSHA writes the contentSHA to the state file @@ -229,13 +230,6 @@ func (m *MManager) tryLoadFromFile() (State, error) { return nil, errors.Wrap(err, "unmarshal state") } - level.Debug(m.Logger).Log( - "event", "state.unmarshal", - "type", "versioned", - "source", "file", - "value", fmt.Sprintf("%+v", state), - ) - if state.V1 != nil { level.Debug(m.Logger).Log("event", "state.resolve", "type", "versioned") return state, nil @@ -297,14 +291,15 @@ func (m *MManager) RemoveStateFile() error { } func (m *MManager) serializeAndWriteState(state VersionedState) error { - debug := level.Debug(log.With(m.Logger, "method", "serializeHelmValues")) + debug := level.Debug(log.With(m.Logger, "method", "serializeAndWriteState")) + state = state.migrateDeprecatedFields() stateFrom := m.V.GetString("state-from") if stateFrom == "" { stateFrom = "file" } - debug.Log("event", "serializeAndWriteState", "stateFrom", stateFrom) + debug.Log("stateFrom", stateFrom) switch stateFrom { case "file": @@ -318,6 +313,7 @@ func (m *MManager) serializeAndWriteState(state VersionedState) error { } func (m *MManager) serializeAndWriteStateFile(state VersionedState) error { + serialized, err := json.MarshalIndent(state, "", " ") if err != nil { return errors.Wrap(err, "serialize state") diff --git a/pkg/state/manager_test.go b/pkg/state/manager_test.go index 6733f5bf9..6c873b588 100644 --- a/pkg/state/manager_test.go +++ b/pkg/state/manager_test.go @@ -191,7 +191,7 @@ func TestMManager_SerializeChartURL(t *testing.T) { }, expected: VersionedState{ V1: &V1{ - ChartURL: "abc123", + Upstream: "abc123", }, }, }, @@ -205,7 +205,7 @@ func TestMManager_SerializeChartURL(t *testing.T) { }, expected: VersionedState{ V1: &V1{ - ChartURL: "abc123", + Upstream: "abc123", ChartRepoURL: "abc123_", }, }, @@ -220,7 +220,7 @@ func TestMManager_SerializeChartURL(t *testing.T) { }, expected: VersionedState{ V1: &V1{ - ChartURL: "xyz789", + Upstream: "xyz789", }, }, }, @@ -237,7 +237,7 @@ func TestMManager_SerializeChartURL(t *testing.T) { err := m.serializeAndWriteState(tt.before) req.NoError(err) - err = m.SerializeChartURL(tt.URL) + err = m.SerializeUpstream(tt.URL) if !tt.wantErr { req.NoError(err, "MManager.SerializeChartURL() error = %v", err) } else { @@ -441,7 +441,7 @@ func TestMManager_SaveHelmOpts(t *testing.T) { }, expected: VersionedState{ V1: &V1{ - ChartURL: "abc123_", + Upstream: "abc123_", ChartRepoURL: "abc123", ChartVersion: "123abc", }, @@ -460,7 +460,7 @@ func TestMManager_SaveHelmOpts(t *testing.T) { }, expected: VersionedState{ V1: &V1{ - ChartURL: "abc123", + Upstream: "abc123", ChartRepoURL: "xyz789", ChartVersion: "789xyz", }, diff --git a/pkg/state/models.go b/pkg/state/models.go index 0cd7067bc..7af2e3ec4 100644 --- a/pkg/state/models.go +++ b/pkg/state/models.go @@ -13,9 +13,9 @@ type State interface { CurrentKustomize() *Kustomize CurrentKustomizeOverlay(filename string) string CurrentHelmValues() string - CurrentChartURL() string CurrentChartRepoURL() string CurrentChartVersion() string + Upstream() string Versioned() VersionedState } @@ -29,9 +29,9 @@ func (Empty) CurrentKustomize() *Kustomize { return nil } func (Empty) CurrentKustomizeOverlay(string) string { return "" } func (Empty) CurrentConfig() map[string]interface{} { return make(map[string]interface{}) } func (Empty) CurrentHelmValues() string { return "" } -func (Empty) CurrentChartURL() string { return "" } func (Empty) CurrentChartRepoURL() string { return "" } func (Empty) CurrentChartVersion() string { return "" } +func (Empty) Upstream() string { return "" } func (Empty) Versioned() VersionedState { return VersionedState{V1: &V1{}} } type V0 map[string]interface{} @@ -40,9 +40,9 @@ func (v V0) CurrentConfig() map[string]interface{} { return v } func (v V0) CurrentKustomize() *Kustomize { return nil } func (v V0) CurrentKustomizeOverlay(string) string { return "" } func (v V0) CurrentHelmValues() string { return "" } -func (v V0) CurrentChartURL() string { return "" } func (v V0) CurrentChartRepoURL() string { return "" } func (v V0) CurrentChartVersion() string { return "" } +func (v V0) Upstream() string { return "" } func (v V0) Versioned() VersionedState { return VersionedState{V1: &V1{Config: v}} } type VersionedState struct { @@ -50,15 +50,17 @@ type VersionedState struct { } type V1 struct { - Config map[string]interface{} `json:"config" yaml:"config" hcl:"config"` - Terraform interface{} `json:"terraform,omitempty" yaml:"terraform,omitempty" hcl:"terraform,omitempty"` - HelmValues string `json:"helmValues,omitempty" yaml:"helmValues,omitempty" hcl:"helmValues,omitempty"` - Kustomize *Kustomize `json:"kustomize,omitempty" yaml:"kustomize,omitempty" hcl:"kustomize,omitempty"` - ChartURL string `json:"chartURL,omitempty" yaml:"chartURL,omitempty" hcl:"chartURL,omitempty"` - ChartRepoURL string `json:"ChartRepoURL,omitempty" yaml:"ChartRepoURL,omitempty" hcl:"ChartRepoURL,omitempty"` - ChartVersion string `json:"ChartVersion,omitempty" yaml:"ChartVersion,omitempty" hcl:"ChartVersion,omitempty"` - ContentSHA string `json:"contentSHA,omitempty" yaml:"contentSHA,omitempty" hcl:"contentSHA,omitempty"` - Lifecycle *Lifeycle `json:"lifecycle,omitempty" yaml:"lifecycle,omitempty" hcl:"lifecycle,omitempty"` + Config map[string]interface{} `json:"config" yaml:"config" hcl:"config"` + Terraform interface{} `json:"terraform,omitempty" yaml:"terraform,omitempty" hcl:"terraform,omitempty"` + HelmValues string `json:"helmValues,omitempty" yaml:"helmValues,omitempty" hcl:"helmValues,omitempty"` + Kustomize *Kustomize `json:"kustomize,omitempty" yaml:"kustomize,omitempty" hcl:"kustomize,omitempty"` + Upstream string `json:"upstream,omitempty" yaml:"upstream,omitempty" hcl:"upstream,omitempty"` + //deprecated in favor of upstream + ChartURL string `json:"chartURL,omitempty" yaml:"chartURL,omitempty" hcl:"chartURL,omitempty"` + ChartRepoURL string `json:"ChartRepoURL,omitempty" yaml:"ChartRepoURL,omitempty" hcl:"ChartRepoURL,omitempty"` + ChartVersion string `json:"ChartVersion,omitempty" yaml:"ChartVersion,omitempty" hcl:"ChartVersion,omitempty"` + ContentSHA string `json:"contentSHA,omitempty" yaml:"contentSHA,omitempty" hcl:"contentSHA,omitempty"` + Lifecycle *Lifeycle `json:"lifecycle,omitempty" yaml:"lifecycle,omitempty" hcl:"lifecycle,omitempty"` } type StepsCompleted map[string]interface{} @@ -109,23 +111,23 @@ func (k *Kustomize) Ship() Overlay { return Overlay{} } -func (u VersionedState) CurrentKustomize() *Kustomize { - if u.V1 != nil { - return u.V1.Kustomize +func (v VersionedState) CurrentKustomize() *Kustomize { + if v.V1 != nil { + return v.V1.Kustomize } return nil } -func (u VersionedState) CurrentKustomizeOverlay(filename string) string { - if u.V1.Kustomize == nil { +func (v VersionedState) CurrentKustomizeOverlay(filename string) string { + if v.V1.Kustomize == nil { return "" } - if u.V1.Kustomize.Overlays == nil { + if v.V1.Kustomize.Overlays == nil { return "" } - overlay, ok := u.V1.Kustomize.Overlays["ship"] + overlay, ok := v.V1.Kustomize.Overlays["ship"] if !ok { return "" } @@ -142,37 +144,40 @@ func (u VersionedState) CurrentKustomizeOverlay(filename string) string { return "" } -func (u VersionedState) CurrentConfig() map[string]interface{} { - if u.V1 != nil && u.V1.Config != nil { - return u.V1.Config +func (v VersionedState) CurrentConfig() map[string]interface{} { + if v.V1 != nil && v.V1.Config != nil { + return v.V1.Config } return make(map[string]interface{}) } -func (u VersionedState) CurrentHelmValues() string { - if u.V1 != nil { - return u.V1.HelmValues +func (v VersionedState) CurrentHelmValues() string { + if v.V1 != nil { + return v.V1.HelmValues } return "" } -func (u VersionedState) CurrentChartURL() string { - if u.V1 != nil { - return u.V1.ChartURL +func (v VersionedState) CurrentChartRepoURL() string { + if v.V1 != nil { + return v.V1.ChartRepoURL } return "" } -func (u VersionedState) CurrentChartRepoURL() string { - if u.V1 != nil { - return u.V1.ChartRepoURL +func (v VersionedState) CurrentChartVersion() string { + if v.V1 != nil { + return v.V1.ChartVersion } return "" } -func (u VersionedState) CurrentChartVersion() string { - if u.V1 != nil { - return u.V1.ChartVersion +func (v VersionedState) Upstream() string { + if v.V1 != nil { + if v.V1.Upstream != "" { + return v.V1.Upstream + } + return v.V1.ChartURL } return "" } @@ -185,3 +190,11 @@ func (v VersionedState) WithCompletedStep(step api.Step) VersionedState { v.V1.Lifecycle = v.V1.Lifecycle.WithCompletedStep(step) return v } + +func (v VersionedState) migrateDeprecatedFields() VersionedState { + if v.V1 != nil { + v.V1.Upstream = v.Upstream() + v.V1.ChartURL = "" + } + return v +} diff --git a/pkg/test-mocks/apptype/determine_type_mock.go b/pkg/test-mocks/apptype/determine_type_mock.go new file mode 100644 index 000000000..5bfd1348d --- /dev/null +++ b/pkg/test-mocks/apptype/determine_type_mock.go @@ -0,0 +1,49 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/replicatedhq/ship/pkg/specs/apptype (interfaces: Inspector) + +// Package apptype is a generated GoMock package. +package apptype + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockInspector is a mock of Inspector interface +type MockInspector struct { + ctrl *gomock.Controller + recorder *MockInspectorMockRecorder +} + +// MockInspectorMockRecorder is the mock recorder for MockInspector +type MockInspectorMockRecorder struct { + mock *MockInspector +} + +// NewMockInspector creates a new mock instance +func NewMockInspector(ctrl *gomock.Controller) *MockInspector { + mock := &MockInspector{ctrl: ctrl} + mock.recorder = &MockInspectorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockInspector) EXPECT() *MockInspectorMockRecorder { + return m.recorder +} + +// DetermineApplicationType mocks base method +func (m *MockInspector) DetermineApplicationType(arg0 context.Context, arg1 string) (string, string, error) { + ret := m.ctrl.Call(m, "DetermineApplicationType", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// DetermineApplicationType indicates an expected call of DetermineApplicationType +func (mr *MockInspectorMockRecorder) DetermineApplicationType(arg0, arg1 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DetermineApplicationType", reflect.TypeOf((*MockInspector)(nil).DetermineApplicationType), arg0, arg1) +} diff --git a/pkg/test-mocks/helm/commands_mock.go b/pkg/test-mocks/helm/commands_mock.go index bb4262cea..d3259e50d 100644 --- a/pkg/test-mocks/helm/commands_mock.go +++ b/pkg/test-mocks/helm/commands_mock.go @@ -45,6 +45,18 @@ func (mr *MockCommandsMockRecorder) DependencyUpdate(arg0 interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DependencyUpdate", reflect.TypeOf((*MockCommands)(nil).DependencyUpdate), arg0) } +// Fetch mocks base method +func (m *MockCommands) Fetch(arg0, arg1, arg2, arg3, arg4 string) error { + ret := m.ctrl.Call(m, "Fetch", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(error) + return ret0 +} + +// Fetch indicates an expected call of Fetch +func (mr *MockCommandsMockRecorder) Fetch(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockCommands)(nil).Fetch), arg0, arg1, arg2, arg3, arg4) +} + // Init mocks base method func (m *MockCommands) Init() error { ret := m.ctrl.Call(m, "Init") diff --git a/pkg/test-mocks/replicatedapp/resolve_replicated_app.go b/pkg/test-mocks/replicatedapp/resolve_replicated_app.go new file mode 100644 index 000000000..61349b404 --- /dev/null +++ b/pkg/test-mocks/replicatedapp/resolve_replicated_app.go @@ -0,0 +1,62 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/replicatedhq/ship/pkg/specs/replicatedapp (interfaces: Resolver) + +// Package replicatedapp is a generated GoMock package. +package replicatedapp + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + api "github.com/replicatedhq/ship/pkg/api" + replicatedapp "github.com/replicatedhq/ship/pkg/specs/replicatedapp" +) + +// MockResolver is a mock of Resolver interface +type MockResolver struct { + ctrl *gomock.Controller + recorder *MockResolverMockRecorder +} + +// MockResolverMockRecorder is the mock recorder for MockResolver +type MockResolverMockRecorder struct { + mock *MockResolver +} + +// NewMockResolver creates a new mock instance +func NewMockResolver(ctrl *gomock.Controller) *MockResolver { + mock := &MockResolver{ctrl: ctrl} + mock.recorder = &MockResolverMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockResolver) EXPECT() *MockResolverMockRecorder { + return m.recorder +} + +// RegisterInstall mocks base method +func (m *MockResolver) RegisterInstall(arg0 context.Context, arg1 replicatedapp.Selector, arg2 *api.Release) error { + ret := m.ctrl.Call(m, "RegisterInstall", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// RegisterInstall indicates an expected call of RegisterInstall +func (mr *MockResolverMockRecorder) RegisterInstall(arg0, arg1, arg2 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterInstall", reflect.TypeOf((*MockResolver)(nil).RegisterInstall), arg0, arg1, arg2) +} + +// ResolveAppRelease mocks base method +func (m *MockResolver) ResolveAppRelease(arg0 context.Context, arg1 *replicatedapp.Selector) (*api.Release, error) { + ret := m.ctrl.Call(m, "ResolveAppRelease", arg0, arg1) + ret0, _ := ret[0].(*api.Release) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ResolveAppRelease indicates an expected call of ResolveAppRelease +func (mr *MockResolverMockRecorder) ResolveAppRelease(arg0, arg1 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResolveAppRelease", reflect.TypeOf((*MockResolver)(nil).ResolveAppRelease), arg0, arg1) +} diff --git a/pkg/test-mocks/state/manager_mock.go b/pkg/test-mocks/state/manager_mock.go index 401c96d41..e4e3b44a8 100644 --- a/pkg/test-mocks/state/manager_mock.go +++ b/pkg/test-mocks/state/manager_mock.go @@ -83,18 +83,6 @@ func (mr *MockManagerMockRecorder) SaveKustomize(arg0 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveKustomize", reflect.TypeOf((*MockManager)(nil).SaveKustomize), arg0) } -// SerializeChartURL mocks base method -func (m *MockManager) SerializeChartURL(arg0 string) error { - ret := m.ctrl.Call(m, "SerializeChartURL", arg0) - ret0, _ := ret[0].(error) - return ret0 -} - -// SerializeChartURL indicates an expected call of SerializeChartURL -func (mr *MockManagerMockRecorder) SerializeChartURL(arg0 interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SerializeChartURL", reflect.TypeOf((*MockManager)(nil).SerializeChartURL), arg0) -} - // SerializeConfig mocks base method func (m *MockManager) SerializeConfig(arg0 []api.Asset, arg1 api.ReleaseMetadata, arg2 map[string]interface{}) error { ret := m.ctrl.Call(m, "SerializeConfig", arg0, arg1, arg2) @@ -131,6 +119,18 @@ func (mr *MockManagerMockRecorder) SerializeHelmValues(arg0, arg1 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SerializeHelmValues", reflect.TypeOf((*MockManager)(nil).SerializeHelmValues), arg0, arg1) } +// SerializeUpstream mocks base method +func (m *MockManager) SerializeUpstream(arg0 string) error { + ret := m.ctrl.Call(m, "SerializeUpstream", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SerializeUpstream indicates an expected call of SerializeUpstream +func (mr *MockManagerMockRecorder) SerializeUpstream(arg0 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SerializeUpstream", reflect.TypeOf((*MockManager)(nil).SerializeUpstream), arg0) +} + // TryLoad mocks base method func (m *MockManager) TryLoad() (state.State, error) { ret := m.ctrl.Call(m, "TryLoad") diff --git a/pkg/testing/matchers/matchers.go b/pkg/testing/matchers/matchers.go index 2859e3d00..33e73b126 100644 --- a/pkg/testing/matchers/matchers.go +++ b/pkg/testing/matchers/matchers.go @@ -34,3 +34,17 @@ func (s *Is) String() string { func (s *Is) Matches(x interface{}) bool { return s.Test(x) } + +var _ gomock.Matcher = &Contains{} + +type Contains struct { + Value string +} + +func (s *Contains) String() string { + return fmt.Sprintf("contains %s", s.Value) +} +func (s *Contains) Matches(x interface{}) bool { + str := fmt.Sprintf("%v", x) + return strings.Contains(str, s.Value) +} diff --git a/pkg/util/filesystem.go b/pkg/util/filesystem.go index b6a3fb242..84dfb5390 100644 --- a/pkg/util/filesystem.go +++ b/pkg/util/filesystem.go @@ -4,6 +4,9 @@ import ( "fmt" "path/filepath" + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/mitchellh/cli" "github.com/pkg/errors" "github.com/spf13/afero" ) @@ -22,3 +25,25 @@ func FindOnlySubdir(dir string, fs afero.Afero) (string, error) { } return filepath.Join(dir, firstFoundFile.Name()), nil } + +func BackupIfPresent(fs afero.Afero, basePath string, logger log.Logger, ui cli.Ui) error { + exists, err := fs.Exists(basePath) + if err != nil { + return errors.Wrapf(err, "check file exists") + } + if !exists { + return nil + } + + backupDest := fmt.Sprintf("%s.bak", basePath) + ui.Warn(fmt.Sprintf("WARNING found directory %s, backing up to %s", basePath, backupDest)) + + level.Info(logger).Log("step.type", "render", "event", "unpackTarget.backup.remove", "src", basePath, "dest", backupDest) + if err := fs.RemoveAll(backupDest); err != nil { + return errors.Wrapf(err, "backup existing dir %s to %s: remove existing %s", basePath, backupDest, backupDest) + } + if err := fs.Rename(basePath, backupDest); err != nil { + return errors.Wrapf(err, "backup existing dir %s to %s", basePath, backupDest) + } + return nil +} diff --git a/web/src/components/kustomize/HelmChartInfo.jsx b/web/src/components/kustomize/HelmChartInfo.jsx index 8a42752b7..4bfe09bbd 100644 --- a/web/src/components/kustomize/HelmChartInfo.jsx +++ b/web/src/components/kustomize/HelmChartInfo.jsx @@ -5,8 +5,8 @@ import RenderActions from "../shared/RenderActions"; import "../../scss/components/kustomize/HelmChartInfo.scss"; import "../../scss/components/shared/DetermineStep.scss"; -const HelmChartInfo = ({ helmChartMetadata, actions, handleAction, isLoading}) => { - const { readme, version } = helmChartMetadata; +const HelmChartInfo = ({ shipAppMetadata, actions, handleAction, isLoading}) => { + const { readme, version } = shipAppMetadata; return (
diff --git a/web/src/components/kustomize/HelmValuesEditor.jsx b/web/src/components/kustomize/HelmValuesEditor.jsx index 1ce53e84b..d6ad1e83e 100644 --- a/web/src/components/kustomize/HelmValuesEditor.jsx +++ b/web/src/components/kustomize/HelmValuesEditor.jsx @@ -172,7 +172,7 @@ export default class HelmValuesEditor extends React.Component { values, readme, name - } = this.props.helmChartMetadata; + } = this.props.shipAppMetadata; return ( diff --git a/web/src/components/shared/DetermineComponentForRoute.jsx b/web/src/components/shared/DetermineComponentForRoute.jsx index 7fd0fb555..5d6314f3a 100644 --- a/web/src/components/shared/DetermineComponentForRoute.jsx +++ b/web/src/components/shared/DetermineComponentForRoute.jsx @@ -175,7 +175,7 @@ class DetermineComponentForRoute extends React.Component { return ( @@ -186,7 +186,7 @@ class DetermineComponentForRoute extends React.Component { saveValues={this.props.saveHelmChartValues} getStep={currentStep.helmValues} isNewRouter={this.props.isNewRouter} - helmChartMetadata={this.props.helmChartMetadata} + shipAppMetadata={this.props.shipAppMetadata} actions={actions} handleAction={this.handleAction} isLoading={this.props.dataLoading.submitActionLoading} diff --git a/web/src/components/shared/DetermineStep.jsx b/web/src/components/shared/DetermineStep.jsx index 189108290..bc52474ce 100644 --- a/web/src/components/shared/DetermineStep.jsx +++ b/web/src/components/shared/DetermineStep.jsx @@ -67,7 +67,7 @@ export default class DetermineStep extends React.Component { return ( @@ -77,7 +77,7 @@ export default class DetermineStep extends React.Component { { - describe("provided helmChartMetadata", () => { + describe("provided shipAppMetadata", () => { const wrapper = mount( { /> ); - it("sets navDetails via helmChartMetadata", () => { + it("sets navDetails via shipAppMetadata", () => { wrapper.setProps({ children: React.cloneElement( wrapper.props().children, { ...mockRouterProps, - helmChartMetadata: { + shipAppMetadata: { name: "testHelm", icon: "testHelmIcon", }, @@ -50,7 +50,7 @@ describe("NavBar", () => { ({ dataLoading: state.ui.main.loading, currentStep: state.data.determineSteps.stepsData.step, - helmChartMetadata: state.data.kustomizeSettings.helmChartMetadata, + shipAppMetadata: state.data.kustomizeSettings.shipAppMetadata, actions: state.data.determineSteps.stepsData.actions, phase: state.data.determineSteps.stepsData.phase, progress: state.data.determineSteps.stepsData.progress, diff --git a/web/src/containers/DetermineStep.js b/web/src/containers/DetermineStep.js index da192ef31..c9a4ba289 100644 --- a/web/src/containers/DetermineStep.js +++ b/web/src/containers/DetermineStep.js @@ -9,7 +9,7 @@ const DetermineStep = connect( state => ({ dataLoading: state.ui.main.loading, currentStep: state.data.determineSteps.stepsData.step, - helmChartMetadata: state.data.kustomizeSettings.helmChartMetadata, + shipAppMetadata: state.data.kustomizeSettings.shipAppMetadata, phase: state.data.determineSteps.stepsData.phase, actions: state.data.determineSteps.stepsData.actions, progress: state.data.determineSteps.stepsData.progress, diff --git a/web/src/containers/HelmChartInfo.js b/web/src/containers/HelmChartInfo.js index c794798f9..e13cf473b 100644 --- a/web/src/containers/HelmChartInfo.js +++ b/web/src/containers/HelmChartInfo.js @@ -7,7 +7,7 @@ import { getHelmChartMetadata } from "../redux/data/kustomizeSettings/actions"; const HelmChartInfo = connect( state => ({ dataLoading: state.ui.main.loading, - helmChartMetadata: state.data.kustomizeSettings.helmChartMetadata, + shipAppMetadata: state.data.kustomizeSettings.shipAppMetadata, actions: state.data.determineSteps.stepsData.actions, }), dispatch => ({ diff --git a/web/src/containers/Navbar.js b/web/src/containers/Navbar.js index 664ad5fc7..b993d4db5 100644 --- a/web/src/containers/Navbar.js +++ b/web/src/containers/Navbar.js @@ -6,7 +6,7 @@ import { loadingData } from "../redux/ui/main/actions"; const NavBar = connect( state => ({ channelDetails: state.data.channelSettings.channelSettingsData.channel, - helmChartMetadata: state.data.kustomizeSettings.helmChartMetadata, + shipAppMetadata: state.data.kustomizeSettings.shipAppMetadata, phase: state.data.determineSteps.stepsData.phase, isKustomizeFlow: state.data.appRoutes.routesData.isKustomizeFlow, isKustomize: state.data.determineSteps.stepsData.kustomizeFlow, diff --git a/web/src/redux/data/kustomizeSettings/index.js b/web/src/redux/data/kustomizeSettings/index.js index fa9dc90ce..48894d4dd 100644 --- a/web/src/redux/data/kustomizeSettings/index.js +++ b/web/src/redux/data/kustomizeSettings/index.js @@ -1,6 +1,6 @@ import { combineReducers } from "redux"; -import { helmChartMetadata } from "./reducer"; +import { shipAppMetadata } from "./reducer"; export default combineReducers({ - helmChartMetadata, + shipAppMetadata, }); diff --git a/web/src/redux/data/kustomizeSettings/reducer.js b/web/src/redux/data/kustomizeSettings/reducer.js index fb1ce7ecb..cc18e4a8a 100644 --- a/web/src/redux/data/kustomizeSettings/reducer.js +++ b/web/src/redux/data/kustomizeSettings/reducer.js @@ -1,6 +1,6 @@ import { constants } from "./actions"; -const helmChartMetadataState = { +const shipAppMetadataState = { name: "", version: "", release: "", @@ -12,7 +12,7 @@ const helmChartMetadataState = { errorMessage: "" }; -export function helmChartMetadata(state = helmChartMetadataState, action) { +export function shipAppMetadata(state = shipAppMetadataState, action) { switch (action.type) { case constants.RECEIVE_HELM_CHART_METADATA: return Object.assign({}, state, {