├── internal └── test │ ├── e2e │ ├── testdata │ │ ├── deploymentconfig │ │ │ ├── 0 │ │ │ │ ├── template.yml │ │ │ │ └── want.out │ │ │ ├── 1 │ │ │ │ ├── want.out │ │ │ │ └── template.yml │ │ │ ├── 2 │ │ │ │ ├── want.err │ │ │ │ └── want.out │ │ │ ├── 3 │ │ │ │ └── .gitkeep │ │ │ └── steps.json │ │ ├── route │ │ │ ├── 0 │ │ │ │ ├── template.yml │ │ │ │ └── want.out │ │ │ ├── 1 │ │ │ │ ├── template.yml │ │ │ │ └── want.out │ │ │ ├── 2 │ │ │ │ ├── want.err │ │ │ │ └── want.out │ │ │ ├── 3 │ │ │ │ └── want.out │ │ │ └── steps.json │ │ ├── secret │ │ │ ├── 0 │ │ │ │ ├── template.yml │ │ │ │ └── want.out │ │ │ ├── 1 │ │ │ │ ├── template.yml │ │ │ │ └── want.out │ │ │ ├── 2 │ │ │ │ ├── want.err │ │ │ │ └── want.out │ │ │ ├── 3 │ │ │ │ └── want.out │ │ │ └── steps.json │ │ ├── service │ │ │ ├── 0 │ │ │ │ ├── template.yml │ │ │ │ └── want.out │ │ │ ├── 1 │ │ │ │ ├── template.yml │ │ │ │ └── want.out │ │ │ ├── 2 │ │ │ │ ├── want.err │ │ │ │ └── want.out │ │ │ ├── 3 │ │ │ │ └── want.out │ │ │ └── steps.json │ │ ├── buildconfig │ │ │ ├── 0 │ │ │ │ ├── template.yml │ │ │ │ └── want.out │ │ │ ├── 1 │ │ │ │ ├── want.out │ │ │ │ └── template.yml │ │ │ ├── 2 │ │ │ │ ├── want.err │ │ │ │ └── want.out │ │ │ ├── 3 │ │ │ │ └── want.out │ │ │ └── steps.json │ │ ├── configmap │ │ │ ├── 0 │ │ │ │ ├── template.yml │ │ │ │ └── want.out │ │ │ ├── 1 │ │ │ │ ├── template.yml │ │ │ │ └── want.out │ │ │ ├── 2 │ │ │ │ ├── want.err │ │ │ │ └── want.out │ │ │ ├── 3 │ │ │ │ └── want.out │ │ │ └── steps.json │ │ ├── imagestream │ │ │ ├── 0 │ │ │ │ ├── template.yml │ │ │ │ └── want.out │ │ │ ├── 1 │ │ │ │ ├── template.yml │ │ │ │ └── want.out │ │ │ ├── 2 │ │ │ │ ├── want.err │ │ │ │ └── want.out │ │ │ ├── 3 │ │ │ │ └── want.out │ │ │ └── steps.json │ │ ├── manual-change │ │ │ ├── 0 │ │ │ │ ├── template.yml │ │ │ │ └── want.out │ │ │ ├── 1 │ │ │ │ ├── template.yml │ │ │ │ └── want.out │ │ │ └── steps.json │ │ ├── selector │ │ │ ├── 0 │ │ │ │ ├── template-bar.yml │ │ │ │ ├── template-foo.yml │ │ │ │ └── want.out │ │ │ ├── 1 │ │ │ │ ├── template-bar.yml │ │ │ │ ├── template-foo.yml │ │ │ │ └── want.out │ │ │ ├── 2 │ │ │ │ ├── want.out │ │ │ │ ├── template-bar.yml │ │ │ │ └── template-foo.yml │ │ │ ├── 3 │ │ │ │ ├── template-bar.yml │ │ │ │ ├── template-foo.yml │ │ │ │ └── want.out │ │ │ └── steps.json │ │ ├── job │ │ │ ├── 0 │ │ │ │ ├── template.yml │ │ │ │ └── want.out │ │ │ ├── 1 │ │ │ │ ├── want.out │ │ │ │ └── template.yml │ │ │ ├── 2 │ │ │ │ └── want.out │ │ │ ├── 3 │ │ │ │ └── want.out │ │ │ └── steps.json │ │ └── recreate │ │ │ ├── 0 │ │ │ ├── template.yml │ │ │ └── want.out │ │ │ ├── 1 │ │ │ ├── template.yml │ │ │ └── want.err │ │ │ ├── 2 │ │ │ ├── template.yml │ │ │ └── want.out │ │ │ └── steps.json │ └── e2e_helper.go │ ├── fixtures │ ├── param-files │ │ ├── bar.env │ │ ├── foo.env │ │ └── baz-without-eol.env │ ├── template-param-detection │ │ ├── garbage.yml │ │ ├── invalid-template.yml │ │ ├── template-blank-parameters.yml │ │ ├── without-tailor-namespace-param.yml │ │ └── with-tailor-namespace-param.yml │ ├── version │ │ ├── client-3_11-and-server-unknown.txt │ │ ├── client-3_11-and-server-3_11.txt │ │ └── client-3_9-and-server-3_11.txt │ ├── item-managed-annotations │ │ ├── is-platform.yml │ │ ├── is-template.yml │ │ ├── is-template-other-change.yml │ │ ├── is-platform-unmanaged.yml │ │ ├── is-template-annotation.yml │ │ ├── is-template-annotation-changed.yml │ │ ├── is-template-different-annotation.yml │ │ └── is-platform-annotation.yml │ ├── templates │ │ ├── is.yml │ │ ├── is-annotation.yml │ │ ├── dc.yml │ │ └── dc-annotation.yml │ ├── item-omitted-fields │ │ ├── rolebinding-template.yml │ │ └── rolebinding-platform.yml │ ├── export │ │ ├── rolebinding-generate-name.yml │ │ ├── bc.yml │ │ └── is.yml │ ├── empty-values │ │ ├── bc-template-defaulted.yml │ │ ├── bc-platform-missing-env.yml │ │ ├── bc-template-empty-env.yml │ │ └── bc-platform-defaulted.yml │ ├── item-applied-config │ │ ├── dc-template.yml │ │ ├── dc-template-changed.yml │ │ ├── dc-platform.yml │ │ ├── dc-platform-annotation-other.yml │ │ ├── dc-platform-annotation-tailor.yml │ │ └── dc-platform-annotation-applied.yml │ └── command-apply │ │ ├── template-dir │ │ └── desired-list.yml │ │ └── current-list.yml │ ├── golden │ ├── export │ │ ├── empty.yml │ │ ├── rolebinding-generate-name.yml │ │ ├── is-trimmed-annotation.yml │ │ ├── is-trimmed-annotation-prefix.yml │ │ ├── is.yml │ │ ├── is-annotation.yml │ │ └── bc.yml │ ├── desired-state │ │ ├── is.yml │ │ ├── is-annotation.yml │ │ ├── dc.yml │ │ └── dc-annotation.yml │ ├── item-managed-annotations │ │ ├── unmanaged-in-platform-added-to-template.txt │ │ ├── unmanaged-in-platform-none-in-template-other-change-in-template.txt │ │ ├── present-in-platform-changed-in-template.txt │ │ ├── present-in-platform-not-in-template.txt │ │ ├── present-in-template-not-in-platform.txt │ │ └── present-in-platform-different-key-in-template.txt │ └── item-omitted-fields │ │ └── rolebinding-changed.txt │ └── helper │ └── file.go ├── pkg ├── openshift │ ├── test-cleartext.env │ ├── test-encoded.env │ ├── test-encrypted.env │ ├── params_test.go │ ├── list.go │ ├── item_test.go │ ├── export.go │ ├── test-public.key │ ├── change.go │ ├── export_test.go │ ├── list_test.go │ ├── filter.go │ ├── change_test.go │ ├── template_test.go │ └── template.go ├── utils │ ├── string_test.go │ ├── file.go │ ├── unmarshal.go │ ├── string.go │ └── encryption.go ├── commands │ ├── export.go │ ├── apply_test.go │ └── secrets.go └── cli │ ├── oc_version_test.go │ ├── cli_test.go │ ├── oc_version.go │ ├── options_test.go │ ├── cli.go │ └── oc_client.go ├── .gitignore ├── go.mod ├── .github └── workflows │ ├── ci.yml │ └── codeql-analysis.yml ├── release.sh ├── Makefile └── go.sum /internal/test/e2e/testdata/deploymentconfig/3/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/test/fixtures/param-files/bar.env: -------------------------------------------------------------------------------- 1 | BAR=bar 2 | -------------------------------------------------------------------------------- /internal/test/fixtures/param-files/foo.env: -------------------------------------------------------------------------------- 1 | FOO=foo 2 | -------------------------------------------------------------------------------- /internal/test/fixtures/param-files/baz-without-eol.env: -------------------------------------------------------------------------------- 1 | BAZ=baz -------------------------------------------------------------------------------- /internal/test/fixtures/template-param-detection/garbage.yml: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /pkg/openshift/test-cleartext.env: -------------------------------------------------------------------------------- 1 | FOO=secret 2 | BAR.B64=c2VjcmV0 3 | -------------------------------------------------------------------------------- /pkg/openshift/test-encoded.env: -------------------------------------------------------------------------------- 1 | FOO=c2VjcmV0 2 | BAR=c2VjcmV0 3 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/route/2/want.err: -------------------------------------------------------------------------------- 1 | Diff not performed due to misconfiguration 2 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/secret/2/want.err: -------------------------------------------------------------------------------- 1 | Diff not performed due to misconfiguration 2 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/service/2/want.err: -------------------------------------------------------------------------------- 1 | Diff not performed due to misconfiguration 2 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/buildconfig/2/want.err: -------------------------------------------------------------------------------- 1 | Diff not performed due to misconfiguration 2 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/configmap/2/want.err: -------------------------------------------------------------------------------- 1 | Diff not performed due to misconfiguration 2 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/imagestream/2/want.err: -------------------------------------------------------------------------------- 1 | Diff not performed due to misconfiguration 2 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/deploymentconfig/2/want.err: -------------------------------------------------------------------------------- 1 | Diff not performed due to misconfiguration 2 | -------------------------------------------------------------------------------- /internal/test/golden/export/empty.yml: -------------------------------------------------------------------------------- 1 | apiVersion: template.openshift.io/v1 2 | kind: Template 3 | objects: [] 4 | -------------------------------------------------------------------------------- /internal/test/fixtures/version/client-3_11-and-server-unknown.txt: -------------------------------------------------------------------------------- 1 | oc v3.11.0+0cbc58b 2 | kubernetes v1.11.0+d4cacc0 3 | features: Basic-Auth 4 | 5 | -------------------------------------------------------------------------------- /internal/test/golden/desired-state/is.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ImageStream 3 | metadata: 4 | name: foo 5 | spec: 6 | dockerImageRepository: foo 7 | lookupPolicy: 8 | local: false 9 | -------------------------------------------------------------------------------- /internal/test/fixtures/item-managed-annotations/is-platform.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ImageStream 3 | metadata: 4 | name: foo 5 | spec: 6 | dockerImageRepository: foo 7 | lookupPolicy: 8 | local: false 9 | -------------------------------------------------------------------------------- /internal/test/fixtures/item-managed-annotations/is-template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ImageStream 3 | metadata: 4 | name: foo 5 | spec: 6 | dockerImageRepository: foo 7 | lookupPolicy: 8 | local: false 9 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/manual-change/0/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: v1 5 | data: 6 | bar: baz 7 | kind: ConfigMap 8 | metadata: 9 | name: foo 10 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/manual-change/1/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: v1 5 | data: 6 | bar: baz 7 | kind: ConfigMap 8 | metadata: 9 | name: foo 10 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/configmap/0/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - kind: ConfigMap 5 | metadata: 6 | name: foo 7 | apiVersion: v1 8 | data: 9 | database-name: bar 10 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/configmap/1/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - kind: ConfigMap 5 | metadata: 6 | name: foo 7 | apiVersion: v1 8 | data: 9 | database-name: baz 10 | -------------------------------------------------------------------------------- /internal/test/fixtures/item-managed-annotations/is-template-other-change.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ImageStream 3 | metadata: 4 | name: foo 5 | spec: 6 | dockerImageRepository: foo 7 | lookupPolicy: 8 | local: true 9 | -------------------------------------------------------------------------------- /internal/test/fixtures/version/client-3_11-and-server-3_11.txt: -------------------------------------------------------------------------------- 1 | oc v3.11.0+0cbc58b 2 | kubernetes v1.11.0+d4cacc0 3 | features: Basic-Auth 4 | Server https://api.domain.com:443 5 | openshift v3.11.43 6 | kubernetes v1.11.0+d4cacc0 7 | -------------------------------------------------------------------------------- /internal/test/fixtures/version/client-3_9-and-server-3_11.txt: -------------------------------------------------------------------------------- 1 | oc v3.9.0+191fece 2 | kubernetes v1.9.1+a0ce1bc657 3 | features: Basic-Auth 4 | Server https://api.domain.com:443 5 | openshift v3.11.43 6 | kubernetes v1.11.0+d4cacc0 7 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/secret/0/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: v1 5 | kind: Secret 6 | metadata: 7 | name: foo 8 | type: opaque 9 | data: 10 | token: czNjcjN0 11 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/secret/1/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: v1 5 | kind: Secret 6 | metadata: 7 | name: foo 8 | type: opaque 9 | data: 10 | token: Z2VIM2lt 11 | -------------------------------------------------------------------------------- /internal/test/golden/item-managed-annotations/unmanaged-in-platform-added-to-template.txt: -------------------------------------------------------------------------------- 1 | Only annotations used by Tailor internally differ. Updating the resource is recommended, but not required. Use --diff=json to see the exact changes. 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | internal/test/e2e/templates 2 | internal/test/e2e/tailor-test 3 | templates-* 4 | params-* 5 | TODO.md 6 | tailor-linux-amd64 7 | tailor-darwin-amd64 8 | tailor-windows-amd64.exe 9 | openshift.local.clusterup/ 10 | .vscode 11 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/imagestream/0/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: v1 5 | kind: ImageStream 6 | metadata: 7 | name: foo 8 | spec: 9 | lookupPolicy: 10 | local: false 11 | -------------------------------------------------------------------------------- /internal/test/golden/desired-state/is-annotation.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ImageStream 3 | metadata: 4 | annotations: 5 | foo: bar 6 | name: foo 7 | spec: 8 | dockerImageRepository: foo 9 | lookupPolicy: 10 | local: false 11 | -------------------------------------------------------------------------------- /internal/test/fixtures/templates/is.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | items: 4 | - apiVersion: v1 5 | kind: ImageStream 6 | metadata: 7 | name: foo 8 | spec: 9 | dockerImageRepository: foo 10 | lookupPolicy: 11 | local: false 12 | -------------------------------------------------------------------------------- /internal/test/fixtures/item-managed-annotations/is-platform-unmanaged.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ImageStream 3 | metadata: 4 | name: foo 5 | annotations: 6 | bar: baz 7 | spec: 8 | dockerImageRepository: foo 9 | lookupPolicy: 10 | local: false 11 | -------------------------------------------------------------------------------- /internal/test/fixtures/item-managed-annotations/is-template-annotation.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ImageStream 3 | metadata: 4 | name: foo 5 | annotations: 6 | bar: baz 7 | spec: 8 | dockerImageRepository: foo 9 | lookupPolicy: 10 | local: false 11 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/imagestream/1/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: v1 5 | kind: ImageStream 6 | metadata: 7 | name: foo 8 | labels: 9 | app: foo 10 | spec: 11 | lookupPolicy: 12 | local: false 13 | -------------------------------------------------------------------------------- /internal/test/fixtures/item-managed-annotations/is-template-annotation-changed.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ImageStream 3 | metadata: 4 | name: foo 5 | annotations: 6 | bar: qux 7 | spec: 8 | dockerImageRepository: foo 9 | lookupPolicy: 10 | local: false 11 | -------------------------------------------------------------------------------- /internal/test/fixtures/item-managed-annotations/is-template-different-annotation.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ImageStream 3 | metadata: 4 | name: foo 5 | annotations: 6 | baz: qux 7 | spec: 8 | dockerImageRepository: foo 9 | lookupPolicy: 10 | local: false 11 | -------------------------------------------------------------------------------- /internal/test/fixtures/templates/is-annotation.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | items: 4 | - apiVersion: v1 5 | kind: ImageStream 6 | metadata: 7 | annotations: 8 | foo: bar 9 | name: foo 10 | spec: 11 | dockerImageRepository: foo 12 | lookupPolicy: 13 | local: false 14 | -------------------------------------------------------------------------------- /internal/test/golden/item-managed-annotations/unmanaged-in-platform-none-in-template-other-change-in-template.txt: -------------------------------------------------------------------------------- 1 | --- Current State (OpenShift cluster) 2 | +++ Desired State (Processed template) 3 | @@ -6,5 +6,5 @@ 4 | spec: 5 | dockerImageRepository: foo 6 | lookupPolicy: 7 | - local: false 8 | + local: true 9 | -------------------------------------------------------------------------------- /internal/test/golden/item-managed-annotations/present-in-platform-changed-in-template.txt: -------------------------------------------------------------------------------- 1 | --- Current State (OpenShift cluster) 2 | +++ Desired State (Processed template) 3 | @@ -2,7 +2,7 @@ 4 | kind: ImageStream 5 | metadata: 6 | annotations: 7 | - bar: baz 8 | + bar: qux 9 | name: foo 10 | spec: 11 | dockerImageRepository: foo 12 | -------------------------------------------------------------------------------- /internal/test/golden/item-managed-annotations/present-in-platform-not-in-template.txt: -------------------------------------------------------------------------------- 1 | --- Current State (OpenShift cluster) 2 | +++ Desired State (Processed template) 3 | @@ -1,8 +1,6 @@ 4 | apiVersion: v1 5 | kind: ImageStream 6 | metadata: 7 | - annotations: 8 | - bar: baz 9 | name: foo 10 | spec: 11 | dockerImageRepository: foo 12 | -------------------------------------------------------------------------------- /internal/test/golden/item-managed-annotations/present-in-template-not-in-platform.txt: -------------------------------------------------------------------------------- 1 | --- Current State (OpenShift cluster) 2 | +++ Desired State (Processed template) 3 | @@ -1,6 +1,8 @@ 4 | apiVersion: v1 5 | kind: ImageStream 6 | metadata: 7 | + annotations: 8 | + bar: baz 9 | name: foo 10 | spec: 11 | dockerImageRepository: foo 12 | -------------------------------------------------------------------------------- /internal/test/golden/item-managed-annotations/present-in-platform-different-key-in-template.txt: -------------------------------------------------------------------------------- 1 | --- Current State (OpenShift cluster) 2 | +++ Desired State (Processed template) 3 | @@ -2,7 +2,7 @@ 4 | kind: ImageStream 5 | metadata: 6 | annotations: 7 | - bar: baz 8 | + baz: qux 9 | name: foo 10 | spec: 11 | dockerImageRepository: foo 12 | -------------------------------------------------------------------------------- /internal/test/fixtures/item-omitted-fields/rolebinding-template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: authorization.openshift.io/v1 2 | groupNames: null 3 | kind: RoleBinding 4 | metadata: 5 | name: admin-0 6 | roleRef: 7 | name: admin 8 | subjects: 9 | - kind: ServiceAccount 10 | name: jenkins 11 | namespace: foo-cd 12 | userNames: 13 | - system:serviceaccount:foo-cd:jenkins 14 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/selector/2/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in selector/2 with OCP namespace {{ .Project }}. 2 | Limiting to resources with selector app=foo. 3 | Found 2 resources in OCP cluster (current state) and 2 resources in processed templates (desired state). 4 | 5 | * cm/foo is in sync 6 | * svc/foo is in sync 7 | 8 | Summary: 2 in sync, 0 to create, 0 to update, 0 to delete 9 | 10 | -------------------------------------------------------------------------------- /pkg/utils/string_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func TestIncludes(t *testing.T) { 6 | if !Includes([]string{"foo", "bar"}, "foo") { 7 | t.Errorf("foo is included") 8 | } 9 | if !Includes([]string{"foo", "bar"}, "bar") { 10 | t.Errorf("bar is included") 11 | } 12 | if Includes([]string{"foo", "bar"}, "baz") { 13 | t.Errorf("baz is not included") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/service/0/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: v1 5 | kind: Service 6 | metadata: 7 | name: foo 8 | spec: 9 | ports: 10 | - name: 8080-tcp 11 | port: 8080 12 | protocol: TCP 13 | targetPort: 8080 14 | selector: 15 | bar: baz 16 | sessionAffinity: None 17 | type: ClusterIP 18 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/service/1/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: v1 5 | kind: Service 6 | metadata: 7 | name: foo 8 | spec: 9 | ports: 10 | - name: 8080-tcp 11 | port: 8080 12 | protocol: TCP 13 | targetPort: 8080 14 | selector: 15 | bar: qux 16 | sessionAffinity: None 17 | type: ClusterIP 18 | -------------------------------------------------------------------------------- /internal/test/golden/export/rolebinding-generate-name.yml: -------------------------------------------------------------------------------- 1 | apiVersion: template.openshift.io/v1 2 | kind: Template 3 | objects: 4 | - apiVersion: authorization.openshift.io/v1 5 | kind: RoleBinding 6 | metadata: 7 | generateName: system:image-pusher- 8 | roleRef: 9 | name: system:image-pusher 10 | subjects: 11 | - kind: ServiceAccount 12 | name: default 13 | namespace: foo-dev 14 | -------------------------------------------------------------------------------- /internal/test/fixtures/item-managed-annotations/is-platform-annotation.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ImageStream 3 | metadata: 4 | name: foo 5 | annotations: 6 | bar: baz 7 | kubectl.kubernetes.io/last-applied-configuration: > 8 | {"apiVersion":"v1","kind":"ImageStream","metadata":{"annotations":{"bar":"baz"}}} 9 | spec: 10 | dockerImageRepository: foo 11 | lookupPolicy: 12 | local: false 13 | -------------------------------------------------------------------------------- /internal/test/fixtures/template-param-detection/invalid-template.yml: -------------------------------------------------------------------------------- 1 | - apiVersion: v1 2 | kind: Route 3 | metadata: 4 | labels: 5 | app: foo 6 | name: foo 7 | spec: 8 | host: foo.domain.com 9 | tls: 10 | insecureEdgeTerminationPolicy: Redirect 11 | termination: edge 12 | to: 13 | kind: Service 14 | name: foo 15 | weight: 100 16 | wildcardPolicy: None 17 | -------------------------------------------------------------------------------- /internal/test/golden/item-omitted-fields/rolebinding-changed.txt: -------------------------------------------------------------------------------- 1 | --- Current State (OpenShift cluster) 2 | +++ Desired State (Processed template) 3 | @@ -5,8 +5,7 @@ 4 | roleRef: 5 | name: admin 6 | subjects: 7 | -- kind: Group 8 | - name: dedicated-admins 9 | -- kind: SystemGroup 10 | - name: system:serviceaccounts:dedicated-admin 11 | +- kind: ServiceAccount 12 | + name: jenkins 13 | + namespace: foo-cd 14 | -------------------------------------------------------------------------------- /internal/test/golden/export/is-trimmed-annotation.yml: -------------------------------------------------------------------------------- 1 | apiVersion: template.openshift.io/v1 2 | kind: Template 3 | objects: 4 | - apiVersion: image.openshift.io/v1 5 | kind: ImageStream 6 | metadata: 7 | annotations: 8 | openshift.io/generated-by: OpenShiftNewApp 9 | labels: 10 | app: foo-bar 11 | name: bar 12 | spec: 13 | dockerImageRepository: bar 14 | lookupPolicy: 15 | local: false 16 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/job/2/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in job/2 with OCP namespace {{ .Project }}. 2 | Limiting resources to job. 3 | Found 1 resource in OCP cluster (current state) and 0 resources in processed templates (desired state). 4 | 5 | No items where found in desired state. Possible reasons are: 6 | * No templates are located in job/2 7 | * No templates contain resources of kinds: job 8 | 9 | Refusing to continue without --force 10 | -------------------------------------------------------------------------------- /internal/test/golden/export/is-trimmed-annotation-prefix.yml: -------------------------------------------------------------------------------- 1 | apiVersion: template.openshift.io/v1 2 | kind: Template 3 | objects: 4 | - apiVersion: image.openshift.io/v1 5 | kind: ImageStream 6 | metadata: 7 | annotations: 8 | description: Keeps track of changes in the application image 9 | labels: 10 | app: foo-bar 11 | name: bar 12 | spec: 13 | dockerImageRepository: bar 14 | lookupPolicy: 15 | local: false 16 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/recreate/0/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: v1 5 | kind: Route 6 | metadata: 7 | labels: 8 | app: foo-route 9 | name: foo 10 | spec: 11 | host: foo.example.com 12 | tls: 13 | insecureEdgeTerminationPolicy: Redirect 14 | termination: edge 15 | to: 16 | kind: Service 17 | name: foo 18 | weight: 100 19 | wildcardPolicy: None 20 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/recreate/1/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: v1 5 | kind: Route 6 | metadata: 7 | labels: 8 | app: foo-route 9 | name: foo 10 | spec: 11 | host: foobar.example.com 12 | tls: 13 | insecureEdgeTerminationPolicy: Redirect 14 | termination: edge 15 | to: 16 | kind: Service 17 | name: foo 18 | weight: 100 19 | wildcardPolicy: None 20 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/recreate/2/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: v1 5 | kind: Route 6 | metadata: 7 | labels: 8 | app: foo-route 9 | name: foo 10 | spec: 11 | host: foobar.example.com 12 | tls: 13 | insecureEdgeTerminationPolicy: Redirect 14 | termination: edge 15 | to: 16 | kind: Service 17 | name: foo 18 | weight: 100 19 | wildcardPolicy: None 20 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/route/2/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in route/2 with OCP namespace {{ .Project }}. 2 | Limiting resources to route. 3 | Found 1 resource in OCP cluster (current state) and 0 resources in processed templates (desired state). 4 | 5 | No items where found in desired state. Possible reasons are: 6 | * No templates are located in route/2 7 | * No templates contain resources of kinds: route 8 | 9 | Refusing to continue without --force 10 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/service/2/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in service/2 with OCP namespace {{ .Project }}. 2 | Limiting resources to svc. 3 | Found 1 resource in OCP cluster (current state) and 0 resources in processed templates (desired state). 4 | 5 | No items where found in desired state. Possible reasons are: 6 | * No templates are located in service/2 7 | * No templates contain resources of kinds: svc 8 | 9 | Refusing to continue without --force 10 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/configmap/2/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in configmap/2 with OCP namespace {{ .Project }}. 2 | Limiting resources to cm. 3 | Found 1 resource in OCP cluster (current state) and 0 resources in processed templates (desired state). 4 | 5 | No items where found in desired state. Possible reasons are: 6 | * No templates are located in configmap/2 7 | * No templates contain resources of kinds: cm 8 | 9 | Refusing to continue without --force 10 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/secret/2/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in secret/2 with OCP namespace {{ .Project }}. 2 | Limiting resources to secret. 3 | Found 1 resource in OCP cluster (current state) and 0 resources in processed templates (desired state). 4 | 5 | No items where found in desired state. Possible reasons are: 6 | * No templates are located in secret/2 7 | * No templates contain resources of kinds: secret 8 | 9 | Refusing to continue without --force 10 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/buildconfig/2/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in buildconfig/2 with OCP namespace {{ .Project }}. 2 | Limiting resources to bc. 3 | Found 1 resource in OCP cluster (current state) and 0 resources in processed templates (desired state). 4 | 5 | No items where found in desired state. Possible reasons are: 6 | * No templates are located in buildconfig/2 7 | * No templates contain resources of kinds: bc 8 | 9 | Refusing to continue without --force 10 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/imagestream/2/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in imagestream/2 with OCP namespace {{ .Project }}. 2 | Limiting resources to is. 3 | Found 1 resource in OCP cluster (current state) and 0 resources in processed templates (desired state). 4 | 5 | No items where found in desired state. Possible reasons are: 6 | * No templates are located in imagestream/2 7 | * No templates contain resources of kinds: is 8 | 9 | Refusing to continue without --force 10 | -------------------------------------------------------------------------------- /internal/test/fixtures/item-omitted-fields/rolebinding-platform.yml: -------------------------------------------------------------------------------- 1 | apiVersion: authorization.openshift.io/v1 2 | groupNames: 3 | - dedicated-admins 4 | - system:serviceaccounts:dedicated-admin 5 | kind: RoleBinding 6 | metadata: 7 | creationTimestamp: null 8 | name: admin-0 9 | roleRef: 10 | name: admin 11 | subjects: 12 | - kind: Group 13 | name: dedicated-admins 14 | - kind: SystemGroup 15 | name: system:serviceaccounts:dedicated-admin 16 | userNames: null 17 | -------------------------------------------------------------------------------- /internal/test/golden/export/is.yml: -------------------------------------------------------------------------------- 1 | apiVersion: template.openshift.io/v1 2 | kind: Template 3 | objects: 4 | - apiVersion: image.openshift.io/v1 5 | kind: ImageStream 6 | metadata: 7 | annotations: 8 | description: Keeps track of changes in the application image 9 | openshift.io/generated-by: OpenShiftNewApp 10 | labels: 11 | app: foo-bar 12 | name: bar 13 | spec: 14 | dockerImageRepository: bar 15 | lookupPolicy: 16 | local: false 17 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/deploymentconfig/2/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in deploymentconfig/2 with OCP namespace {{ .Project }}. 2 | Limiting resources to dc,secret. 3 | Found 2 resources in OCP cluster (current state) and 0 resources in processed templates (desired state). 4 | 5 | No items where found in desired state. Possible reasons are: 6 | * No templates are located in deploymentconfig/2 7 | * No templates contain resources of kinds: dc,secret 8 | 9 | Refusing to continue without --force 10 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/route/0/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | parameters: 4 | - name: TAILOR_NAMESPACE 5 | required: true 6 | objects: 7 | - apiVersion: v1 8 | kind: Route 9 | metadata: 10 | name: foo 11 | spec: 12 | host: foo-${TAILOR_NAMESPACE}.example.com 13 | tls: 14 | insecureEdgeTerminationPolicy: Redirect 15 | termination: edge 16 | to: 17 | kind: Service 18 | name: foo 19 | weight: 100 20 | wildcardPolicy: None 21 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/route/1/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | parameters: 4 | - name: TAILOR_NAMESPACE 5 | required: true 6 | objects: 7 | - apiVersion: v1 8 | kind: Route 9 | metadata: 10 | name: foo 11 | spec: 12 | host: foo-${TAILOR_NAMESPACE}.example.com 13 | tls: 14 | insecureEdgeTerminationPolicy: None 15 | termination: edge 16 | to: 17 | kind: Service 18 | name: foo 19 | weight: 100 20 | wildcardPolicy: None 21 | -------------------------------------------------------------------------------- /internal/test/fixtures/export/rolebinding-generate-name.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: List 3 | items: 4 | - apiVersion: authorization.openshift.io/v1 5 | groupNames: null 6 | kind: RoleBinding 7 | metadata: 8 | creationTimestamp: null 9 | generateName: system:image-pusher- 10 | roleRef: 11 | name: system:image-pusher 12 | subjects: 13 | - kind: ServiceAccount 14 | name: default 15 | namespace: foo-dev 16 | userNames: 17 | - system:serviceaccount:foo-dev:default 18 | -------------------------------------------------------------------------------- /internal/test/fixtures/template-param-detection/template-blank-parameters.yml: -------------------------------------------------------------------------------- 1 | apiVersion: template.openshift.io/v1 2 | kind: Template 3 | objects: 4 | - apiVersion: v1 5 | kind: Route 6 | metadata: 7 | labels: 8 | app: foo 9 | name: foo 10 | spec: 11 | host: foo.domain.com 12 | tls: 13 | insecureEdgeTerminationPolicy: Redirect 14 | termination: edge 15 | to: 16 | kind: Service 17 | name: foo 18 | weight: 100 19 | wildcardPolicy: None 20 | parameters: 21 | -------------------------------------------------------------------------------- /internal/test/fixtures/template-param-detection/without-tailor-namespace-param.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: v1 5 | kind: Route 6 | metadata: 7 | labels: 8 | app: foo 9 | name: foo 10 | spec: 11 | host: foo-${NAMESPACE}.domain.com 12 | tls: 13 | insecureEdgeTerminationPolicy: Redirect 14 | termination: edge 15 | to: 16 | kind: Service 17 | name: foo 18 | weight: 100 19 | wildcardPolicy: None 20 | parameters: 21 | - name: NAMESPACE 22 | required: true 23 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/recreate/1/want.err: -------------------------------------------------------------------------------- 1 | Path '/spec/host' of 'route/foo' is immutable. 2 | Changing its value would require to delete and re-create the whole resource, which Tailor prevents by default. 3 | 4 | You may pick one of the following options to resolve this: 5 | 6 | * pass --allow-recreate to give permission to recreate the resource 7 | * use --preserve-immutable-fields to keep the cluster state for all immutable paths 8 | * change the template to be in sync with the cluster state 9 | * exclude the resource from comparison via --exclude route/foo 10 | -------------------------------------------------------------------------------- /internal/test/fixtures/empty-values/bc-template-defaulted.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: BuildConfig 3 | metadata: 4 | labels: 5 | app: foo 6 | name: foo 7 | spec: 8 | failedBuildsHistoryLimit: 5 9 | output: 10 | to: 11 | kind: ImageStreamTag 12 | name: foo:latest 13 | runPolicy: Serial 14 | source: 15 | type: Binary 16 | strategy: 17 | type: Docker 18 | successfulBuildsHistoryLimit: 5 19 | triggers: 20 | - generic: 21 | secret: password 22 | type: Generic 23 | - type: ImageChange 24 | - type: ConfigChange 25 | -------------------------------------------------------------------------------- /internal/test/fixtures/template-param-detection/with-tailor-namespace-param.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: v1 5 | kind: Route 6 | metadata: 7 | labels: 8 | app: foo 9 | name: foo 10 | spec: 11 | host: foo-${TAILOR_NAMESPACE}.domain.com 12 | tls: 13 | insecureEdgeTerminationPolicy: Redirect 14 | termination: edge 15 | to: 16 | kind: Service 17 | name: foo 18 | weight: 100 19 | wildcardPolicy: None 20 | parameters: 21 | - name: TAILOR_NAMESPACE 22 | required: true 23 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/manual-change/steps.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "apply cm/foo", 4 | "wantStdout": true, 5 | "wantResources": { 6 | "cm/foo": true 7 | }, 8 | "wantFields": { 9 | "cm/foo": { 10 | ".data.bar": "baz" 11 | } 12 | } 13 | }, 14 | { 15 | "before": "oc -n {{ .Project }} patch cm/foo -p {\"data\":{\"bar\":\"qux\"}}", 16 | "command": "diff cm/foo", 17 | "wantStdout": true, 18 | "wantErr": true 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /internal/test/fixtures/empty-values/bc-platform-missing-env.yml: -------------------------------------------------------------------------------- 1 | kind: BuildConfig 2 | apiVersion: v1 3 | metadata: 4 | name: foo 5 | labels: 6 | app: foo 7 | spec: 8 | nodeSelector: null 9 | postCommit: {} 10 | resources: {} 11 | runPolicy: Serial 12 | triggers: [] 13 | source: 14 | binary: {} 15 | type: Binary 16 | strategy: 17 | type: Docker 18 | dockerStrategy: 19 | env: 20 | - name: FOO_BAR 21 | - name: BAZ 22 | value: qux 23 | output: 24 | to: 25 | kind: ImageStreamTag 26 | name: foo:latest 27 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/manual-change/1/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in manual-change/1 with OCP namespace {{ .Project }}. 2 | Limiting resources to cm/foo. 3 | Found 1 resource in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | ~ cm/foo to update 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1,6 +1,6 @@ 9 | apiVersion: v1 10 | data: 11 | - bar: qux 12 | + bar: baz 13 | kind: ConfigMap 14 | metadata: 15 | annotations: {} 16 | 17 | Summary: 0 in sync, 0 to create, 1 to update, 0 to delete 18 | 19 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/configmap/0/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in configmap/0 with OCP namespace {{ .Project }}. 2 | Limiting resources to cm. 3 | Found 0 resources in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | + cm/foo to create 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1 +1,7 @@ 9 | +apiVersion: v1 10 | +data: 11 | + database-name: bar 12 | +kind: ConfigMap 13 | +metadata: 14 | + name: foo 15 | 16 | 17 | Summary: 0 in sync, 1 to create, 0 to update, 0 to delete 18 | 19 | Creating cm/foo ... done 20 | -------------------------------------------------------------------------------- /internal/test/fixtures/empty-values/bc-template-empty-env.yml: -------------------------------------------------------------------------------- 1 | kind: BuildConfig 2 | apiVersion: v1 3 | metadata: 4 | name: foo 5 | labels: 6 | app: foo 7 | spec: 8 | nodeSelector: null 9 | postCommit: {} 10 | resources: {} 11 | runPolicy: Serial 12 | triggers: [] 13 | source: 14 | binary: {} 15 | type: Binary 16 | strategy: 17 | type: Docker 18 | dockerStrategy: 19 | env: 20 | - name: FOO_BAR 21 | value: "" 22 | - name: BAZ 23 | value: qux 24 | output: 25 | to: 26 | kind: ImageStreamTag 27 | name: foo:latest 28 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/manual-change/0/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in manual-change/0 with OCP namespace {{ .Project }}. 2 | Limiting resources to cm/foo. 3 | Found 0 resources in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | + cm/foo to create 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1 +1,7 @@ 9 | +apiVersion: v1 10 | +data: 11 | + bar: baz 12 | +kind: ConfigMap 13 | +metadata: 14 | + name: foo 15 | 16 | 17 | Summary: 0 in sync, 1 to create, 0 to update, 0 to delete 18 | 19 | Creating cm/foo ... done 20 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/selector/0/template-bar.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | metadata: 4 | name: configmap 5 | objects: 6 | - apiVersion: v1 7 | kind: ConfigMap 8 | metadata: 9 | name: bar 10 | labels: 11 | app: bar 12 | data: 13 | bar: baz 14 | - apiVersion: v1 15 | kind: Service 16 | metadata: 17 | labels: 18 | app: bar 19 | name: bar 20 | spec: 21 | ports: 22 | - name: web 23 | port: 80 24 | protocol: TCP 25 | targetPort: 8080 26 | selector: 27 | name: bar 28 | sessionAffinity: None 29 | type: ClusterIP 30 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/selector/0/template-foo.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | metadata: 4 | name: configmap 5 | objects: 6 | - apiVersion: v1 7 | kind: ConfigMap 8 | metadata: 9 | name: foo 10 | labels: 11 | app: foo 12 | data: 13 | bar: baz 14 | - apiVersion: v1 15 | kind: Service 16 | metadata: 17 | labels: 18 | app: foo 19 | name: foo 20 | spec: 21 | ports: 22 | - name: web 23 | port: 80 24 | protocol: TCP 25 | targetPort: 8080 26 | selector: 27 | name: foo 28 | sessionAffinity: None 29 | type: ClusterIP 30 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/selector/1/template-bar.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | metadata: 4 | name: configmap 5 | objects: 6 | - apiVersion: v1 7 | kind: ConfigMap 8 | metadata: 9 | name: bar 10 | labels: 11 | app: bar 12 | data: 13 | bar: baz 14 | - apiVersion: v1 15 | kind: Service 16 | metadata: 17 | labels: 18 | app: bar 19 | name: bar 20 | spec: 21 | ports: 22 | - name: web 23 | port: 80 24 | protocol: TCP 25 | targetPort: 8080 26 | selector: 27 | name: bar 28 | sessionAffinity: None 29 | type: ClusterIP 30 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/selector/1/template-foo.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | metadata: 4 | name: configmap 5 | objects: 6 | - apiVersion: v1 7 | kind: ConfigMap 8 | metadata: 9 | name: foo 10 | labels: 11 | app: foo 12 | data: 13 | bar: baz 14 | - apiVersion: v1 15 | kind: Service 16 | metadata: 17 | labels: 18 | app: foo 19 | name: foo 20 | spec: 21 | ports: 22 | - name: web 23 | port: 80 24 | protocol: TCP 25 | targetPort: 8080 26 | selector: 27 | name: foo 28 | sessionAffinity: None 29 | type: ClusterIP 30 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/selector/2/template-bar.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | metadata: 4 | name: configmap 5 | objects: 6 | - apiVersion: v1 7 | kind: ConfigMap 8 | metadata: 9 | name: bar 10 | labels: 11 | app: bar 12 | data: 13 | bar: qux 14 | - apiVersion: v1 15 | kind: Service 16 | metadata: 17 | labels: 18 | app: bar 19 | name: bar 20 | spec: 21 | ports: 22 | - name: web 23 | port: 80 24 | protocol: TCP 25 | targetPort: 8080 26 | selector: 27 | name: bar 28 | sessionAffinity: None 29 | type: ClusterIP 30 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/selector/2/template-foo.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | metadata: 4 | name: configmap 5 | objects: 6 | - apiVersion: v1 7 | kind: ConfigMap 8 | metadata: 9 | name: foo 10 | labels: 11 | app: foo 12 | data: 13 | bar: baz 14 | - apiVersion: v1 15 | kind: Service 16 | metadata: 17 | labels: 18 | app: foo 19 | name: foo 20 | spec: 21 | ports: 22 | - name: web 23 | port: 80 24 | protocol: TCP 25 | targetPort: 8080 26 | selector: 27 | name: foo 28 | sessionAffinity: None 29 | type: ClusterIP 30 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/selector/3/template-bar.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | metadata: 4 | name: configmap 5 | objects: 6 | - apiVersion: v1 7 | kind: ConfigMap 8 | metadata: 9 | name: bar 10 | labels: 11 | app: bar 12 | data: 13 | bar: qux 14 | - apiVersion: v1 15 | kind: Service 16 | metadata: 17 | labels: 18 | app: bar 19 | name: bar 20 | spec: 21 | ports: 22 | - name: web 23 | port: 80 24 | protocol: TCP 25 | targetPort: 8080 26 | selector: 27 | name: bar 28 | sessionAffinity: None 29 | type: ClusterIP 30 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/selector/3/template-foo.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | metadata: 4 | name: configmap 5 | objects: 6 | - apiVersion: v1 7 | kind: ConfigMap 8 | metadata: 9 | name: foo 10 | labels: 11 | app: foo 12 | data: 13 | bar: baz 14 | - apiVersion: v1 15 | kind: Service 16 | metadata: 17 | labels: 18 | app: foo 19 | name: foo 20 | spec: 21 | ports: 22 | - name: web 23 | port: 80 24 | protocol: TCP 25 | targetPort: 8080 26 | selector: 27 | name: foo 28 | sessionAffinity: None 29 | type: ClusterIP 30 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/secret/0/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in secret/0 with OCP namespace {{ .Project }}. 2 | Limiting resources to secret. 3 | Found 0 resources in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | + secret/foo to create 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1 +1,8 @@ 9 | +apiVersion: v1 10 | +data: 11 | + token: czNjcjN0 12 | +kind: Secret 13 | +metadata: 14 | + name: foo 15 | +type: opaque 16 | 17 | 18 | Summary: 0 in sync, 1 to create, 0 to update, 0 to delete 19 | 20 | Creating secret/foo ... done 21 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/secret/1/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in secret/1 with OCP namespace {{ .Project }}. 2 | Limiting resources to secret. 3 | Found 1 resource in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | ~ secret/foo to update 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1,6 +1,6 @@ 9 | apiVersion: v1 10 | data: 11 | - token: czNjcjN0 12 | + token: Z2VIM2lt 13 | kind: Secret 14 | metadata: 15 | annotations: {} 16 | 17 | Summary: 0 in sync, 0 to create, 1 to update, 0 to delete 18 | 19 | Updating secret/foo ... done 20 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/selector/3/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in selector/3 with OCP namespace {{ .Project }}. 2 | Limiting to resources with selector app=bar. 3 | Found 2 resources in OCP cluster (current state) and 2 resources in processed templates (desired state). 4 | 5 | * svc/bar is in sync 6 | ~ cm/bar to update 7 | --- Current State (OpenShift cluster) 8 | +++ Desired State (Processed template) 9 | @@ -1,6 +1,6 @@ 10 | apiVersion: v1 11 | data: 12 | - bar: baz 13 | + bar: qux 14 | kind: ConfigMap 15 | metadata: 16 | annotations: {} 17 | 18 | Summary: 1 in sync, 0 to create, 1 to update, 0 to delete 19 | 20 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/configmap/1/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in configmap/1 with OCP namespace {{ .Project }}. 2 | Limiting resources to cm. 3 | Found 1 resource in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | ~ cm/foo to update 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1,6 +1,6 @@ 9 | apiVersion: v1 10 | data: 11 | - database-name: bar 12 | + database-name: baz 13 | kind: ConfigMap 14 | metadata: 15 | annotations: {} 16 | 17 | Summary: 0 in sync, 0 to create, 1 to update, 0 to delete 18 | 19 | Updating cm/foo ... done 20 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/job/1/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in job/1 with OCP namespace {{ .Project }}. 2 | Limiting resources to job. 3 | Found 1 resource in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | ~ job/pi to update 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -6,7 +6,7 @@ 9 | job-name: pi 10 | name: pi 11 | spec: 12 | - backoffLimit: 6 13 | + backoffLimit: 5 14 | completions: 1 15 | parallelism: 1 16 | selector: 17 | 18 | Summary: 0 in sync, 0 to create, 1 to update, 0 to delete 19 | 20 | Updating job/pi ... done 21 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/imagestream/1/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in imagestream/1 with OCP namespace {{ .Project }}. 2 | Limiting resources to is. 3 | Found 1 resource in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | ~ is/foo to update 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -2,6 +2,8 @@ 9 | kind: ImageStream 10 | metadata: 11 | annotations: {} 12 | + labels: 13 | + app: foo 14 | name: foo 15 | spec: 16 | lookupPolicy: 17 | 18 | Summary: 0 in sync, 0 to create, 1 to update, 0 to delete 19 | 20 | Updating is/foo ... done 21 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/imagestream/0/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in imagestream/0 with OCP namespace {{ .Project }}. 2 | Limiting resources to is. 3 | Found 0 resources in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | + is/foo to create 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1 +1,8 @@ 9 | +apiVersion: image.openshift.io/v1 10 | +kind: ImageStream 11 | +metadata: 12 | + name: foo 13 | +spec: 14 | + lookupPolicy: 15 | + local: false 16 | 17 | 18 | Summary: 0 in sync, 1 to create, 0 to update, 0 to delete 19 | 20 | Creating is/foo ... done 21 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/service/1/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in service/1 with OCP namespace {{ .Project }}. 2 | Limiting resources to svc. 3 | Found 1 resource in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | ~ svc/foo to update 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -10,7 +10,7 @@ 9 | protocol: TCP 10 | targetPort: 8080 11 | selector: 12 | - bar: baz 13 | + bar: qux 14 | sessionAffinity: None 15 | type: ClusterIP 16 | 17 | 18 | Summary: 0 in sync, 0 to create, 1 to update, 0 to delete 19 | 20 | Updating svc/foo ... done 21 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/buildconfig/1/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in buildconfig/1 with OCP namespace {{ .Project }}. 2 | Limiting resources to bc. 3 | Found 1 resource in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | ~ bc/foo to update 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -20,6 +20,7 @@ 9 | memory: 128Mi 10 | runPolicy: Serial 11 | source: 12 | + contextDir: baz 13 | git: 14 | ref: master 15 | uri: https://github.com/opendevstack/tailor.git 16 | 17 | Summary: 0 in sync, 0 to create, 1 to update, 0 to delete 18 | 19 | Updating bc/foo ... done 20 | -------------------------------------------------------------------------------- /internal/test/fixtures/item-applied-config/dc-template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: DeploymentConfig 3 | metadata: 4 | name: foo 5 | spec: 6 | replicas: 1 7 | selector: 8 | name: foo 9 | strategy: 10 | type: Recreate 11 | template: 12 | metadata: 13 | annotations: {} 14 | labels: 15 | name: foo 16 | spec: 17 | containers: 18 | - image: bar/foo:latest 19 | imagePullPolicy: IfNotPresent 20 | name: foo 21 | dnsPolicy: ClusterFirst 22 | restartPolicy: Always 23 | schedulerName: default-scheduler 24 | serviceAccount: foo 25 | serviceAccountName: foo 26 | test: false 27 | triggers: [] 28 | -------------------------------------------------------------------------------- /internal/test/fixtures/empty-values/bc-platform-defaulted.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: BuildConfig 3 | metadata: 4 | annotations: {} 5 | labels: 6 | app: foo 7 | name: foo 8 | spec: 9 | failedBuildsHistoryLimit: 5 10 | output: 11 | to: 12 | kind: ImageStreamTag 13 | name: foo:latest 14 | postCommit: {} 15 | resources: {} 16 | runPolicy: Serial 17 | source: 18 | binary: {} 19 | type: Binary 20 | strategy: 21 | dockerStrategy: {} 22 | type: Docker 23 | successfulBuildsHistoryLimit: 5 24 | triggers: 25 | - generic: 26 | secret: password 27 | type: Generic 28 | - imageChange: {} 29 | type: ImageChange 30 | - type: ConfigChange 31 | -------------------------------------------------------------------------------- /internal/test/golden/export/is-annotation.yml: -------------------------------------------------------------------------------- 1 | apiVersion: template.openshift.io/v1 2 | kind: Template 3 | objects: 4 | - apiVersion: image.openshift.io/v1 5 | kind: ImageStream 6 | metadata: 7 | annotations: 8 | description: Keeps track of changes in the application image 9 | kubectl.kubernetes.io/last-applied-configuration: '{"apiVersion":"v1","kind":"ImageStream","metadata":{"annotations":{"foo":"bar"}}}' 10 | openshift.io/generated-by: OpenShiftNewApp 11 | openshift.io/image.dockerRepositoryCheck: "2018-08-07T12:32:24Z" 12 | labels: 13 | app: foo-bar 14 | name: bar 15 | spec: 16 | dockerImageRepository: bar 17 | lookupPolicy: 18 | local: false 19 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/route/1/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in route/1 with OCP namespace {{ .Project }}. 2 | Limiting resources to route. 3 | Found 1 resource in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | ~ route/foo to update 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -6,7 +6,7 @@ 9 | spec: 10 | host: foo-{{ .Project }}.example.com 11 | tls: 12 | - insecureEdgeTerminationPolicy: Redirect 13 | + insecureEdgeTerminationPolicy: None 14 | termination: edge 15 | to: 16 | kind: Service 17 | 18 | Summary: 0 in sync, 0 to create, 1 to update, 0 to delete 19 | 20 | Updating route/foo ... done 21 | -------------------------------------------------------------------------------- /internal/test/fixtures/item-applied-config/dc-template-changed.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: DeploymentConfig 3 | metadata: 4 | name: foo 5 | spec: 6 | replicas: 1 7 | selector: 8 | name: foo 9 | strategy: 10 | type: Recreate 11 | template: 12 | metadata: 13 | annotations: {} 14 | labels: 15 | name: foo 16 | spec: 17 | containers: 18 | - image: bar/foo:experiment 19 | imagePullPolicy: IfNotPresent 20 | name: foo 21 | dnsPolicy: ClusterFirst 22 | restartPolicy: Always 23 | schedulerName: default-scheduler 24 | securityContext: {} 25 | serviceAccount: foo 26 | serviceAccountName: foo 27 | volumes: [] 28 | test: false 29 | triggers: [] 30 | -------------------------------------------------------------------------------- /pkg/utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | ) 7 | 8 | // FileStater is a helper interface to allow testing. 9 | type FileStater interface { 10 | Stat(name string) (os.FileInfo, error) 11 | } 12 | 13 | // OsFS implements Stat() for local disk. 14 | type OsFS struct{} 15 | 16 | // Stat proxies to os.Stat. 17 | func (OsFS) Stat(name string) (os.FileInfo, error) { return os.Stat(name) } 18 | 19 | // ReadFile reads the content of given filename and returns it as a string 20 | func ReadFile(filename string) (string, error) { 21 | if _, err := os.Stat(filename); err != nil { 22 | return "", err 23 | } 24 | bytes, err := ioutil.ReadFile(filename) 25 | if err != nil { 26 | return "", err 27 | } 28 | return string(bytes), nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/service/0/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in service/0 with OCP namespace {{ .Project }}. 2 | Limiting resources to svc. 3 | Found 0 resources in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | + svc/foo to create 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1 +1,15 @@ 9 | +apiVersion: v1 10 | +kind: Service 11 | +metadata: 12 | + name: foo 13 | +spec: 14 | + ports: 15 | + - name: 8080-tcp 16 | + port: 8080 17 | + protocol: TCP 18 | + targetPort: 8080 19 | + selector: 20 | + bar: baz 21 | + sessionAffinity: None 22 | + type: ClusterIP 23 | 24 | 25 | Summary: 0 in sync, 1 to create, 0 to update, 0 to delete 26 | 27 | Creating svc/foo ... done 28 | -------------------------------------------------------------------------------- /internal/test/fixtures/item-applied-config/dc-platform.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: DeploymentConfig 3 | metadata: 4 | name: foo 5 | spec: 6 | replicas: 1 7 | selector: 8 | name: foo 9 | strategy: 10 | type: Recreate 11 | template: 12 | metadata: 13 | annotations: {} 14 | labels: 15 | name: foo 16 | spec: 17 | containers: 18 | - image: 192.168.0.1:5000/bar/foo@sha256:51ead8367892a487ca4a1ca7435fa418466901ca2842b777e15a12d0b470ab30 19 | imagePullPolicy: IfNotPresent 20 | name: foo 21 | dnsPolicy: ClusterFirst 22 | restartPolicy: Always 23 | schedulerName: default-scheduler 24 | securityContext: {} 25 | serviceAccount: foo 26 | serviceAccountName: foo 27 | volumes: [] 28 | test: false 29 | triggers: [] 30 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/route/0/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in route/0 with OCP namespace {{ .Project }}. 2 | Limiting resources to route. 3 | Found 0 resources in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | + route/foo to create 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1 +1,15 @@ 9 | +apiVersion: route.openshift.io/v1 10 | +kind: Route 11 | +metadata: 12 | + name: foo 13 | +spec: 14 | + host: foo-{{ .Project }}.example.com 15 | + tls: 16 | + insecureEdgeTerminationPolicy: Redirect 17 | + termination: edge 18 | + to: 19 | + kind: Service 20 | + name: foo 21 | + weight: 100 22 | + wildcardPolicy: None 23 | 24 | 25 | Summary: 0 in sync, 1 to create, 0 to update, 0 to delete 26 | 27 | Creating route/foo ... done 28 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/configmap/3/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in configmap/3 with OCP namespace {{ .Project }}. 2 | Limiting resources to cm. 3 | Found 1 resource in OCP cluster (current state) and 0 resources in processed templates (desired state). 4 | 5 | - cm/foo to delete 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1,10 +1 @@ 9 | -apiVersion: v1 10 | -data: 11 | - database-name: baz 12 | -kind: ConfigMap 13 | -metadata: 14 | - annotations: 15 | - kubectl.kubernetes.io/last-applied-configuration: | 16 | - {"apiVersion":"v1","data":{"database-name":"baz"},"kind":"ConfigMap","metadata":{"annotations":{},"name":"foo","namespace":"{{ .Project }}"}} 17 | - name: foo 18 | 19 | 20 | Summary: 0 in sync, 0 to create, 0 to update, 1 to delete 21 | 22 | Deleting cm/foo ... done 23 | -------------------------------------------------------------------------------- /internal/test/fixtures/export/bc.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: List 3 | items: 4 | - apiVersion: build.openshift.io/v1 5 | kind: BuildConfig 6 | metadata: 7 | labels: 8 | app: foo-deviations 9 | component: foo-dev-monitoring 10 | dotted: some.foo-dev.thing 11 | name: bar 12 | spec: 13 | nodeSelector: null 14 | output: 15 | to: 16 | kind: ImageStreamTag 17 | name: bar:latest 18 | postCommit: {} 19 | resources: {} 20 | runPolicy: Serial 21 | source: 22 | git: 23 | ref: master 24 | uri: https://github.com/foo-dev/bar.git 25 | type: Git 26 | strategy: 27 | dockerStrategy: 28 | from: 29 | kind: ImageStreamTag 30 | name: base:latest 31 | namespace: foo-dev 32 | type: Docker 33 | triggers: [] 34 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/recreate/0/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in recreate/0 with OCP namespace {{ .Project }}. 2 | Limiting resources to route/foo. 3 | Found 0 resources in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | + route/foo to create 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1 +1,17 @@ 9 | +apiVersion: route.openshift.io/v1 10 | +kind: Route 11 | +metadata: 12 | + labels: 13 | + app: foo-route 14 | + name: foo 15 | +spec: 16 | + host: foo.example.com 17 | + tls: 18 | + insecureEdgeTerminationPolicy: Redirect 19 | + termination: edge 20 | + to: 21 | + kind: Service 22 | + name: foo 23 | + weight: 100 24 | + wildcardPolicy: None 25 | 26 | 27 | Summary: 0 in sync, 1 to create, 0 to update, 0 to delete 28 | 29 | Creating route/foo ... done 30 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/secret/3/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in secret/3 with OCP namespace {{ .Project }}. 2 | Limiting resources to secret. 3 | Found 1 resource in OCP cluster (current state) and 0 resources in processed templates (desired state). 4 | 5 | - secret/foo to delete 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1,11 +1 @@ 9 | -apiVersion: v1 10 | -data: 11 | - token: Z2VIM2lt 12 | -kind: Secret 13 | -metadata: 14 | - annotations: 15 | - kubectl.kubernetes.io/last-applied-configuration: | 16 | - {"apiVersion":"v1","data":{"token":"Z2VIM2lt"},"kind":"Secret","metadata":{"annotations":{},"name":"foo","namespace":"{{ .Project }}"},"type":"opaque"} 17 | - name: foo 18 | -type: opaque 19 | 20 | 21 | Summary: 0 in sync, 0 to create, 0 to update, 1 to delete 22 | 23 | Deleting secret/foo ... done 24 | -------------------------------------------------------------------------------- /internal/test/fixtures/item-applied-config/dc-platform-annotation-other.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: DeploymentConfig 3 | metadata: 4 | name: foo 5 | annotations: 6 | foo: bar 7 | spec: 8 | replicas: 1 9 | selector: 10 | name: foo 11 | strategy: 12 | type: Recreate 13 | template: 14 | metadata: 15 | annotations: {} 16 | labels: 17 | name: foo 18 | spec: 19 | containers: 20 | - image: 192.168.0.1:5000/bar/foo@sha256:51ead8367892a487ca4a1ca7435fa418466901ca2842b777e15a12d0b470ab30 21 | imagePullPolicy: IfNotPresent 22 | name: foo 23 | dnsPolicy: ClusterFirst 24 | restartPolicy: Always 25 | schedulerName: default-scheduler 26 | securityContext: {} 27 | serviceAccount: foo 28 | serviceAccountName: foo 29 | volumes: [] 30 | test: false 31 | triggers: [] 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/opendevstack/tailor 2 | 3 | require ( 4 | github.com/alecthomas/kingpin v2.2.6+incompatible 5 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect 7 | github.com/fatih/color v1.7.0 8 | github.com/ghodss/yaml v1.0.0 9 | github.com/google/go-cmp v0.3.1 10 | github.com/mattn/go-colorable v0.0.9 // indirect 11 | github.com/mattn/go-isatty v0.0.3 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 13 | github.com/stretchr/testify v1.3.0 // indirect 14 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f 15 | golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac 16 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 17 | golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 // indirect 18 | gopkg.in/yaml.v2 v2.2.1 // indirect 19 | ) 20 | 21 | go 1.14 22 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/deploymentconfig/1/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in deploymentconfig/1 with OCP namespace {{ .Project }}. 2 | Limiting resources to dc,secret. 3 | Found 2 resources in OCP cluster (current state) and 2 resources in processed templates (desired state). 4 | 5 | * secret/foo-user is in sync 6 | ~ dc/foo to update 7 | --- Current State (OpenShift cluster) 8 | +++ Desired State (Processed template) 9 | @@ -29,11 +29,6 @@ 10 | - env: 11 | - name: FOO 12 | value: abc 13 | - - name: QUX 14 | - valueFrom: 15 | - secretKeyRef: 16 | - key: username 17 | - name: foo-user 18 | - name: BAZ 19 | value: http://baz.{{ .Project }}.svc:8080/ 20 | image: docker-registry.default.svc:5000/{{ .Project }}/foo:latest 21 | 22 | Summary: 1 in sync, 0 to create, 1 to update, 0 to delete 23 | 24 | Updating dc/foo ... done 25 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/job/0/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: batch/v1 5 | kind: Job 6 | metadata: 7 | labels: 8 | job-name: pi 9 | name: pi 10 | spec: 11 | backoffLimit: 6 12 | completions: 1 13 | parallelism: 1 14 | selector: 15 | matchLabels: 16 | job-name: pi 17 | template: 18 | metadata: 19 | labels: 20 | job-name: pi 21 | name: pi 22 | spec: 23 | containers: 24 | - name: pi 25 | image: perl 26 | imagePullPolicy: Always 27 | command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] 28 | terminationMessagePath: /dev/termination-log 29 | terminationMessagePolicy: File 30 | dnsPolicy: ClusterFirst 31 | restartPolicy: OnFailure 32 | schedulerName: default-scheduler 33 | terminationGracePeriodSeconds: 30 34 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/job/1/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: batch/v1 5 | kind: Job 6 | metadata: 7 | labels: 8 | job-name: pi 9 | name: pi 10 | spec: 11 | backoffLimit: 5 12 | completions: 1 13 | parallelism: 1 14 | selector: 15 | matchLabels: 16 | job-name: pi 17 | template: 18 | metadata: 19 | labels: 20 | job-name: pi 21 | name: pi 22 | spec: 23 | containers: 24 | - name: pi 25 | image: perl 26 | imagePullPolicy: Always 27 | command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] 28 | terminationMessagePath: /dev/termination-log 29 | terminationMessagePolicy: File 30 | dnsPolicy: ClusterFirst 31 | restartPolicy: OnFailure 32 | schedulerName: default-scheduler 33 | terminationGracePeriodSeconds: 30 34 | -------------------------------------------------------------------------------- /internal/test/fixtures/item-applied-config/dc-platform-annotation-tailor.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: DeploymentConfig 3 | metadata: 4 | name: foo 5 | annotations: 6 | tailor.opendevstack.org/applied-config: '{"/spec/template/spec/containers/0/image":"bar/foo:latest"}' 7 | spec: 8 | replicas: 1 9 | selector: 10 | name: foo 11 | strategy: 12 | type: Recreate 13 | template: 14 | metadata: 15 | annotations: {} 16 | labels: 17 | name: foo 18 | spec: 19 | containers: 20 | - image: 192.168.0.1:5000/bar/foo@sha256:51ead8367892a487ca4a1ca7435fa418466901ca2842b777e15a12d0b470ab30 21 | imagePullPolicy: IfNotPresent 22 | name: foo 23 | dnsPolicy: ClusterFirst 24 | restartPolicy: Always 25 | schedulerName: default-scheduler 26 | securityContext: {} 27 | serviceAccount: foo 28 | serviceAccountName: foo 29 | volumes: [] 30 | test: false 31 | triggers: [] 32 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/imagestream/3/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in imagestream/3 with OCP namespace {{ .Project }}. 2 | Limiting resources to is. 3 | Found 1 resource in OCP cluster (current state) and 0 resources in processed templates (desired state). 4 | 5 | - is/foo to delete 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1,13 +1 @@ 9 | -apiVersion: image.openshift.io/v1 10 | -kind: ImageStream 11 | -metadata: 12 | - annotations: 13 | - kubectl.kubernetes.io/last-applied-configuration: | 14 | - {"apiVersion":"image.openshift.io/v1","kind":"ImageStream","metadata":{"annotations":{},"labels":{"app":"foo"},"name":"foo","namespace":"{{ .Project }}"},"spec":{"lookupPolicy":{"local":false}}} 15 | - labels: 16 | - app: foo 17 | - name: foo 18 | -spec: 19 | - lookupPolicy: 20 | - local: false 21 | 22 | 23 | Summary: 0 in sync, 0 to create, 0 to update, 1 to delete 24 | 25 | Deleting is/foo ... done 26 | -------------------------------------------------------------------------------- /pkg/commands/export.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/opendevstack/tailor/pkg/cli" 7 | "github.com/opendevstack/tailor/pkg/openshift" 8 | ) 9 | 10 | // Export prints an export of targeted resources to STDOUT. 11 | func Export(exportOptions *cli.ExportOptions) error { 12 | filter, err := openshift.NewResourceFilter(exportOptions.Resource, exportOptions.Selector, exportOptions.Excludes) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | c := cli.NewOcClient(exportOptions.Namespace) 18 | out, err := openshift.ExportAsTemplateFile( 19 | filter, 20 | exportOptions.WithAnnotations, 21 | exportOptions.Namespace, 22 | exportOptions.WithHardcodedNamespace, 23 | exportOptions.TrimAnnotations, 24 | c, 25 | ) 26 | if err != nil { 27 | return fmt.Errorf( 28 | "Could not export %s resources as template: %s", 29 | filter.String(), 30 | err, 31 | ) 32 | } 33 | 34 | fmt.Println(out) 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/test/golden/export/bc.yml: -------------------------------------------------------------------------------- 1 | apiVersion: template.openshift.io/v1 2 | kind: Template 3 | objects: 4 | - apiVersion: build.openshift.io/v1 5 | kind: BuildConfig 6 | metadata: 7 | labels: 8 | app: foo-deviations 9 | component: foo-dev-monitoring 10 | dotted: some.${TAILOR_NAMESPACE}.thing 11 | name: bar 12 | spec: 13 | nodeSelector: null 14 | output: 15 | to: 16 | kind: ImageStreamTag 17 | name: bar:latest 18 | postCommit: {} 19 | resources: {} 20 | runPolicy: Serial 21 | source: 22 | git: 23 | ref: master 24 | uri: https://github.com/${TAILOR_NAMESPACE}/bar.git 25 | type: Git 26 | strategy: 27 | dockerStrategy: 28 | from: 29 | kind: ImageStreamTag 30 | name: base:latest 31 | namespace: ${TAILOR_NAMESPACE} 32 | type: Docker 33 | triggers: [] 34 | parameters: 35 | - name: TAILOR_NAMESPACE 36 | required: true 37 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/buildconfig/0/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: v1 5 | kind: BuildConfig 6 | metadata: 7 | name: foo 8 | spec: 9 | output: 10 | to: 11 | kind: ImageStreamTag 12 | name: 'foo:latest' 13 | postCommit: {} 14 | resources: 15 | limits: 16 | cpu: "1" 17 | memory: 256Mi 18 | requests: 19 | cpu: 500m 20 | memory: 128Mi 21 | runPolicy: Serial 22 | source: 23 | git: 24 | ref: master 25 | uri: https://github.com/opendevstack/tailor.git 26 | sourceSecret: 27 | name: token 28 | type: Git 29 | strategy: 30 | dockerStrategy: 31 | forcePull: true 32 | noCache: true 33 | buildArgs: 34 | - name: foo 35 | value: bar 36 | type: Docker 37 | successfulBuildsHistoryLimit: 5 38 | failedBuildsHistoryLimit: 5 39 | nodeSelector: null 40 | triggers: [] 41 | -------------------------------------------------------------------------------- /pkg/utils/unmarshal.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | ) 8 | 9 | // displaySyntaxError will display more information 10 | // such as line and error type given an error and 11 | // the data that was unmarshalled. 12 | // Thanks to https://github.com/markpeek/packer/commit/5bf33a0e91b2318a40c42e9bf855dcc8dd4cdec5 13 | func DisplaySyntaxError(data []byte, syntaxError error) (err error) { 14 | syntax, ok := syntaxError.(*json.SyntaxError) 15 | if !ok { 16 | err = syntaxError 17 | return 18 | } 19 | newline := []byte{'\x0a'} 20 | space := []byte{' '} 21 | 22 | start, end := bytes.LastIndex(data[:syntax.Offset], newline)+1, len(data) 23 | if idx := bytes.Index(data[start:], newline); idx >= 0 { 24 | end = start + idx 25 | } 26 | 27 | line, pos := bytes.Count(data[:start], newline)+1, int(syntax.Offset)-start-1 28 | 29 | err = fmt.Errorf("\nError in line %d: %s \n%s\n%s^", line, syntaxError, data[start:end], bytes.Repeat(space, pos)) 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/buildconfig/1/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | objects: 4 | - apiVersion: v1 5 | kind: BuildConfig 6 | metadata: 7 | name: foo 8 | spec: 9 | output: 10 | to: 11 | kind: ImageStreamTag 12 | name: 'foo:latest' 13 | postCommit: {} 14 | resources: 15 | limits: 16 | cpu: "1" 17 | memory: 256Mi 18 | requests: 19 | cpu: 500m 20 | memory: 128Mi 21 | runPolicy: Serial 22 | source: 23 | contextDir: baz 24 | git: 25 | ref: master 26 | uri: https://github.com/opendevstack/tailor.git 27 | sourceSecret: 28 | name: token 29 | type: Git 30 | strategy: 31 | dockerStrategy: 32 | forcePull: true 33 | noCache: true 34 | buildArgs: 35 | - name: foo 36 | value: bar 37 | type: Docker 38 | successfulBuildsHistoryLimit: 5 39 | failedBuildsHistoryLimit: 5 40 | nodeSelector: null 41 | triggers: [] 42 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/job/steps.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "apply job", 4 | "wantStdout": true, 5 | "wantResources": { 6 | "job/pi": true 7 | }, 8 | "wantFields": { 9 | "job/pi": { 10 | ".spec.backoffLimit": "6" 11 | } 12 | } 13 | }, 14 | { 15 | "command": "apply job", 16 | "wantStdout": true, 17 | "wantResources": { 18 | "job/pi": true 19 | }, 20 | "wantFields": { 21 | "job/pi": { 22 | ".spec.backoffLimit": "5" 23 | } 24 | } 25 | }, 26 | { 27 | "command": "apply job", 28 | "wantStdout": true, 29 | "wantErr": true, 30 | "wantResources": { 31 | "job/pi": true 32 | } 33 | }, 34 | { 35 | "command": "apply job --force", 36 | "wantStdout": true, 37 | "wantResources": { 38 | "job/pi": false 39 | } 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/imagestream/steps.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "apply is", 4 | "wantStdout": true, 5 | "wantResources": { 6 | "is/foo": true 7 | }, 8 | "wantFields": { 9 | "is/foo": { 10 | ".metadata.name": "foo" 11 | } 12 | } 13 | }, 14 | { 15 | "command": "apply is", 16 | "wantStdout": true, 17 | "wantResources": { 18 | "is/foo": true 19 | }, 20 | "wantFields": { 21 | "is/foo": { 22 | ".metadata.labels.app": "foo" 23 | } 24 | } 25 | }, 26 | { 27 | "command": "apply is", 28 | "wantStdout": true, 29 | "wantErr": true, 30 | "wantResources": { 31 | "is/foo": true 32 | } 33 | }, 34 | { 35 | "command": "apply is --force", 36 | "wantStdout": true, 37 | "wantResources": { 38 | "is/foo": false 39 | } 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/configmap/steps.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "apply cm", 4 | "wantStdout": true, 5 | "wantResources": { 6 | "cm/foo": true 7 | }, 8 | "wantFields": { 9 | "cm/foo": { 10 | ".data.database-name": "bar" 11 | } 12 | } 13 | }, 14 | { 15 | "command": "apply cm", 16 | "wantStdout": true, 17 | "wantResources": { 18 | "cm/foo": true 19 | }, 20 | "wantFields": { 21 | "cm/foo": { 22 | ".data.database-name": "baz" 23 | } 24 | } 25 | }, 26 | { 27 | "command": "apply cm", 28 | "wantStdout": true, 29 | "wantErr": true, 30 | "wantResources": { 31 | "cm/foo": true 32 | } 33 | }, 34 | { 35 | "command": "apply cm --force", 36 | "wantStdout": true, 37 | "wantResources": { 38 | "cm/foo": false 39 | } 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/service/steps.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "apply svc", 4 | "wantStdout": true, 5 | "wantResources": { 6 | "svc/foo": true 7 | }, 8 | "wantFields": { 9 | "svc/foo": { 10 | ".spec.selector.bar": "baz" 11 | } 12 | } 13 | }, 14 | { 15 | "command": "apply svc", 16 | "wantStdout": true, 17 | "wantResources": { 18 | "svc/foo": true 19 | }, 20 | "wantFields": { 21 | "svc/foo": { 22 | ".spec.selector.bar": "qux" 23 | } 24 | } 25 | }, 26 | { 27 | "command": "apply svc", 28 | "wantStdout": true, 29 | "wantErr": true, 30 | "wantResources": { 31 | "svc/foo": true 32 | } 33 | }, 34 | { 35 | "command": "apply svc --force", 36 | "wantStdout": true, 37 | "wantResources": { 38 | "is/foo": false 39 | } 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/recreate/steps.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "apply route/foo", 4 | "wantStdout": true, 5 | "wantResources": { 6 | "route/foo": true 7 | }, 8 | "wantFields": { 9 | "route/foo": { 10 | ".spec.host": "foo.example.com" 11 | } 12 | } 13 | }, 14 | { 15 | "command": "apply route/foo", 16 | "wantStderr": true, 17 | "wantErr": true, 18 | "wantResources": { 19 | "route/foo": true 20 | }, 21 | "wantFields": { 22 | "route/foo": { 23 | ".spec.host": "foo.example.com" 24 | } 25 | } 26 | }, 27 | { 28 | "command": "apply route/foo --allow-recreate", 29 | "wantStdout": true, 30 | "wantResources": { 31 | "route/foo": true 32 | }, 33 | "wantFields": { 34 | "route/foo": { 35 | ".spec.host": "foobar.example.com" 36 | } 37 | } 38 | } 39 | ] 40 | -------------------------------------------------------------------------------- /internal/test/fixtures/item-applied-config/dc-platform-annotation-applied.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: DeploymentConfig 3 | metadata: 4 | name: foo 5 | annotations: 6 | kubectl.kubernetes.io/last-applied-configuration: > 7 | {"apiVersion":"v1","kind":"DeploymentConfig","metadata":{"name":"foo"},"spec":{"template":{"spec":{"containers":[{"image":"bar/foo:latest"}]}}}} 8 | spec: 9 | replicas: 1 10 | selector: 11 | name: foo 12 | strategy: 13 | type: Recreate 14 | template: 15 | metadata: 16 | annotations: {} 17 | labels: 18 | name: foo 19 | spec: 20 | containers: 21 | - image: 192.168.0.1:5000/bar/foo@sha256:51ead8367892a487ca4a1ca7435fa418466901ca2842b777e15a12d0b470ab30 22 | imagePullPolicy: IfNotPresent 23 | name: foo 24 | dnsPolicy: ClusterFirst 25 | restartPolicy: Always 26 | schedulerName: default-scheduler 27 | securityContext: {} 28 | serviceAccount: foo 29 | serviceAccountName: foo 30 | volumes: [] 31 | test: false 32 | triggers: [] 33 | -------------------------------------------------------------------------------- /internal/test/fixtures/export/is.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: List 3 | items: 4 | - apiVersion: image.openshift.io/v1 5 | kind: ImageStream 6 | metadata: 7 | annotations: 8 | description: Keeps track of changes in the application image 9 | openshift.io/image.dockerRepositoryCheck: 2018-08-07T12:32:24Z 10 | openshift.io/generated-by: OpenShiftNewApp 11 | kubectl.kubernetes.io/last-applied-configuration: '{"apiVersion":"v1","kind":"ImageStream","metadata":{"annotations":{"foo":"bar"}}}' 12 | creationTimestamp: null 13 | generation: 560 14 | labels: 15 | app: foo-bar 16 | name: bar 17 | spec: 18 | dockerImageRepository: bar 19 | lookupPolicy: 20 | local: false 21 | tags: 22 | - annotations: null 23 | from: 24 | kind: ImageStreamImage 25 | name: bar@sha256:4e418dd975063c99f52d0d17076b54e38186a90deb34cd5c502ed045e9c385da 26 | generation: 560 27 | importPolicy: {} 28 | name: latest 29 | referencePolicy: 30 | type: Source 31 | status: 32 | dockerImageRepository: "" 33 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/buildconfig/steps.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "apply bc", 4 | "wantStdout": true, 5 | "wantResources": { 6 | "bc/foo": true 7 | }, 8 | "wantFields": { 9 | "bc/foo": { 10 | ".spec.source.git.ref": "master" 11 | } 12 | } 13 | }, 14 | { 15 | "command": "apply bc", 16 | "wantStdout": true, 17 | "wantResources": { 18 | "bc/foo": true 19 | }, 20 | "wantFields": { 21 | "bc/foo": { 22 | ".spec.source.git.ref": "master", 23 | ".spec.source.contextDir": "baz" 24 | } 25 | } 26 | }, 27 | { 28 | "command": "apply bc", 29 | "wantStdout": true, 30 | "wantErr": true, 31 | "wantResources": { 32 | "bc/foo": true 33 | } 34 | }, 35 | { 36 | "command": "apply bc --force", 37 | "wantStdout": true, 38 | "wantResources": { 39 | "bc/foo": false 40 | } 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/route/steps.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "apply route", 4 | "wantStdout": true, 5 | "wantResources": { 6 | "route/foo": true 7 | }, 8 | "wantFields": { 9 | "route/foo": { 10 | ".spec.tls.insecureEdgeTerminationPolicy": "Redirect" 11 | } 12 | } 13 | }, 14 | { 15 | "command": "apply route", 16 | "wantStdout": true, 17 | "wantResources": { 18 | "route/foo": true 19 | }, 20 | "wantFields": { 21 | "route/foo": { 22 | ".spec.tls.insecureEdgeTerminationPolicy": "None" 23 | } 24 | } 25 | }, 26 | { 27 | "command": "apply route", 28 | "wantStdout": true, 29 | "wantErr": true, 30 | "wantResources": { 31 | "route/foo": true 32 | } 33 | }, 34 | { 35 | "command": "apply route --force", 36 | "wantStdout": true, 37 | "wantResources": { 38 | "route/foo": false 39 | } 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /internal/test/fixtures/command-apply/template-dir/desired-list.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | items: 3 | - apiVersion: build.openshift.io/v1 4 | kind: BuildConfig 5 | metadata: 6 | name: foo 7 | spec: 8 | failedBuildsHistoryLimit: 5 9 | nodeSelector: null 10 | output: 11 | to: 12 | kind: ImageStreamTag 13 | name: foo:v1 14 | postCommit: {} 15 | resources: 16 | limits: 17 | cpu: "1" 18 | memory: 1Gi 19 | requests: 20 | cpu: 200m 21 | memory: 512Mi 22 | runPolicy: Serial 23 | source: 24 | git: 25 | ref: master 26 | uri: https://example.com/example/foo.git 27 | sourceSecret: 28 | name: user-credentials 29 | type: Git 30 | strategy: 31 | dockerStrategy: {} 32 | type: Docker 33 | successfulBuildsHistoryLimit: 5 34 | triggers: [] 35 | - apiVersion: image.openshift.io/v1 36 | kind: ImageStream 37 | metadata: 38 | name: foo 39 | spec: 40 | dockerImageRepository: foo 41 | lookupPolicy: 42 | local: true 43 | kind: List 44 | metadata: {} 45 | -------------------------------------------------------------------------------- /pkg/utils/string.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "strings" 4 | 5 | // IncludesPrefix checks if needle is in haystack 6 | func Includes(haystack []string, needle string) bool { 7 | for _, name := range haystack { 8 | if name == needle { 9 | return true 10 | } 11 | } 12 | return false 13 | } 14 | 15 | // IncludesPrefix checks if any item in haystack is a prefix of needle 16 | func IncludesPrefix(haystack []string, needle string) bool { 17 | for _, prefix := range haystack { 18 | if strings.HasPrefix(needle, prefix) { 19 | return true 20 | } 21 | } 22 | return false 23 | } 24 | 25 | // Remove removes val from slice 26 | func Remove(s []string, val string) []string { 27 | for i, v := range s { 28 | if v == val { 29 | return append(s[:i], s[i+1:]...) 30 | } 31 | } 32 | return s 33 | } 34 | 35 | // JSONPointerPath builds a JSON pointer path according to spec, see 36 | // https://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-07#section-3. 37 | func JSONPointerPath(s string) string { 38 | pointer := strings.Replace(s, "~", "~0", -1) 39 | return strings.Replace(pointer, "/", "~1", -1) 40 | } 41 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/selector/0/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in selector/0 with OCP namespace {{ .Project }}. 2 | Limiting to resources with selector app=foo. 3 | Found 0 resources in OCP cluster (current state) and 2 resources in processed templates (desired state). 4 | 5 | + cm/foo to create 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1 +1,9 @@ 9 | +apiVersion: v1 10 | +data: 11 | + bar: baz 12 | +kind: ConfigMap 13 | +metadata: 14 | + labels: 15 | + app: foo 16 | + name: foo 17 | 18 | + svc/foo to create 19 | --- Current State (OpenShift cluster) 20 | +++ Desired State (Processed template) 21 | @@ -1 +1,17 @@ 22 | +apiVersion: v1 23 | +kind: Service 24 | +metadata: 25 | + labels: 26 | + app: foo 27 | + name: foo 28 | +spec: 29 | + ports: 30 | + - name: web 31 | + port: 80 32 | + protocol: TCP 33 | + targetPort: 8080 34 | + selector: 35 | + name: foo 36 | + sessionAffinity: None 37 | + type: ClusterIP 38 | 39 | 40 | Summary: 0 in sync, 2 to create, 0 to update, 0 to delete 41 | 42 | Creating cm/foo ... done 43 | Creating svc/foo ... done 44 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/selector/1/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in selector/1 with OCP namespace {{ .Project }}. 2 | Limiting to resources with selector app=bar. 3 | Found 0 resources in OCP cluster (current state) and 2 resources in processed templates (desired state). 4 | 5 | + cm/bar to create 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1 +1,9 @@ 9 | +apiVersion: v1 10 | +data: 11 | + bar: baz 12 | +kind: ConfigMap 13 | +metadata: 14 | + labels: 15 | + app: bar 16 | + name: bar 17 | 18 | + svc/bar to create 19 | --- Current State (OpenShift cluster) 20 | +++ Desired State (Processed template) 21 | @@ -1 +1,17 @@ 22 | +apiVersion: v1 23 | +kind: Service 24 | +metadata: 25 | + labels: 26 | + app: bar 27 | + name: bar 28 | +spec: 29 | + ports: 30 | + - name: web 31 | + port: 80 32 | + protocol: TCP 33 | + targetPort: 8080 34 | + selector: 35 | + name: bar 36 | + sessionAffinity: None 37 | + type: ClusterIP 38 | 39 | 40 | Summary: 0 in sync, 2 to create, 0 to update, 0 to delete 41 | 42 | Creating cm/bar ... done 43 | Creating svc/bar ... done 44 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/service/3/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in service/3 with OCP namespace {{ .Project }}. 2 | Limiting resources to svc. 3 | Found 1 resource in OCP cluster (current state) and 0 resources in processed templates (desired state). 4 | 5 | - svc/foo to delete 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1,18 +1 @@ 9 | -apiVersion: v1 10 | -kind: Service 11 | -metadata: 12 | - annotations: 13 | - kubectl.kubernetes.io/last-applied-configuration: | 14 | - {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"foo","namespace":"{{ .Project }}"},"spec":{"ports":[{"name":"8080-tcp","port":8080,"protocol":"TCP","targetPort":8080}],"selector":{"bar":"qux"},"sessionAffinity":"None","type":"ClusterIP"}} 15 | - name: foo 16 | -spec: 17 | - ports: 18 | - - name: 8080-tcp 19 | - port: 8080 20 | - protocol: TCP 21 | - targetPort: 8080 22 | - selector: 23 | - bar: qux 24 | - sessionAffinity: None 25 | - type: ClusterIP 26 | 27 | 28 | Summary: 0 in sync, 0 to create, 0 to update, 1 to delete 29 | 30 | Deleting svc/foo ... done 31 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/secret/steps.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "apply secret --reveal-secrets", 4 | "wantStdout": true, 5 | "wantResources": { 6 | "secret/foo": true 7 | }, 8 | "wantFields": { 9 | "secret/foo": { 10 | ".data.token": "czNjcjN0" 11 | } 12 | } 13 | }, 14 | { 15 | "command": "apply secret --reveal-secrets", 16 | "wantStdout": true, 17 | "wantResources": { 18 | "secret/foo": true 19 | }, 20 | "wantFields": { 21 | "secret/foo": { 22 | ".data.token": "Z2VIM2lt" 23 | } 24 | } 25 | }, 26 | { 27 | "command": "apply secret --reveal-secrets", 28 | "wantStdout": true, 29 | "wantErr": true, 30 | "wantResources": { 31 | "secret/foo": true 32 | } 33 | }, 34 | { 35 | "command": "apply secret --reveal-secrets --force", 36 | "wantStdout": true, 37 | "wantResources": { 38 | "secret/foo": false 39 | } 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/route/3/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in route/3 with OCP namespace {{ .Project }}. 2 | Limiting resources to route. 3 | Found 1 resource in OCP cluster (current state) and 0 resources in processed templates (desired state). 4 | 5 | - route/foo to delete 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1,18 +1 @@ 9 | -apiVersion: route.openshift.io/v1 10 | -kind: Route 11 | -metadata: 12 | - annotations: 13 | - kubectl.kubernetes.io/last-applied-configuration: | 14 | - {"apiVersion":"route.openshift.io/v1","kind":"Route","metadata":{"annotations":{},"name":"foo","namespace":"{{ .Project }}"},"spec":{"host":"foo-{{ .Project }}.example.com","tls":{"insecureEdgeTerminationPolicy":"None","termination":"edge"},"to":{"kind":"Service","name":"foo","weight":100},"wildcardPolicy":"None"}} 15 | - name: foo 16 | -spec: 17 | - host: foo-{{ .Project }}.example.com 18 | - tls: 19 | - insecureEdgeTerminationPolicy: None 20 | - termination: edge 21 | - to: 22 | - kind: Service 23 | - name: foo 24 | - weight: 100 25 | - wildcardPolicy: None 26 | 27 | 28 | Summary: 0 in sync, 0 to create, 0 to update, 1 to delete 29 | 30 | Deleting route/foo ... done 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continous Integration Tests 2 | on: [push, pull_request] 3 | jobs: 4 | tailor: 5 | name: Run tests 6 | runs-on: ubuntu-latest 7 | steps: 8 | - 9 | name: Checkout repository 10 | uses: actions/checkout@v2 11 | with: 12 | fetch-depth: 0 13 | - 14 | name: Setup Go 15 | uses: actions/setup-go@v2 16 | with: 17 | version: 1.14 18 | - 19 | name: Download Go tools 20 | run: | 21 | curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.25.0 22 | go get golang.org/x/tools/cmd/goimports 23 | - 24 | name: Run lint 25 | run: | 26 | export PATH=$PATH:$(go env GOPATH)/bin 27 | make lint 28 | - 29 | name: Setup OpenShift 30 | uses: manusa/actions-setup-openshift@v1.1.2 31 | with: 32 | oc version: 'v3.11.0' 33 | enable: 'centos-imagestreams,persistent-volumes,registry,router' 34 | github token: ${{ secrets.GITHUB_TOKEN }} 35 | - 36 | name: Run tests 37 | run: | 38 | export PATH=$PATH:$(go env GOPATH)/bin 39 | sudo chown -R runner:docker openshift.local.clusterup/ 40 | make test 41 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/job/0/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in job/0 with OCP namespace {{ .Project }}. 2 | Limiting resources to job. 3 | Found 0 resources in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | + job/pi to create 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1 +1,35 @@ 9 | +apiVersion: batch/v1 10 | +kind: Job 11 | +metadata: 12 | + labels: 13 | + job-name: pi 14 | + name: pi 15 | +spec: 16 | + backoffLimit: 6 17 | + completions: 1 18 | + parallelism: 1 19 | + selector: 20 | + matchLabels: 21 | + job-name: pi 22 | + template: 23 | + metadata: 24 | + labels: 25 | + job-name: pi 26 | + name: pi 27 | + spec: 28 | + containers: 29 | + - command: 30 | + - perl 31 | + - -Mbignum=bpi 32 | + - -wle 33 | + - print bpi(2000) 34 | + image: perl 35 | + imagePullPolicy: Always 36 | + name: pi 37 | + terminationMessagePath: /dev/termination-log 38 | + terminationMessagePolicy: File 39 | + dnsPolicy: ClusterFirst 40 | + restartPolicy: OnFailure 41 | + schedulerName: default-scheduler 42 | + terminationGracePeriodSeconds: 30 43 | 44 | 45 | Summary: 0 in sync, 1 to create, 0 to update, 0 to delete 46 | 47 | Creating job/pi ... done 48 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/buildconfig/0/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in buildconfig/0 with OCP namespace {{ .Project }}. 2 | Limiting resources to bc. 3 | Found 0 resources in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | + bc/foo to create 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1 +1,38 @@ 9 | +apiVersion: build.openshift.io/v1 10 | +kind: BuildConfig 11 | +metadata: 12 | + name: foo 13 | +spec: 14 | + failedBuildsHistoryLimit: 5 15 | + nodeSelector: null 16 | + output: 17 | + to: 18 | + kind: ImageStreamTag 19 | + name: foo:latest 20 | + postCommit: {} 21 | + resources: 22 | + limits: 23 | + cpu: "1" 24 | + memory: 256Mi 25 | + requests: 26 | + cpu: 500m 27 | + memory: 128Mi 28 | + runPolicy: Serial 29 | + source: 30 | + git: 31 | + ref: master 32 | + uri: https://github.com/opendevstack/tailor.git 33 | + sourceSecret: 34 | + name: token 35 | + type: Git 36 | + strategy: 37 | + dockerStrategy: 38 | + buildArgs: 39 | + - name: foo 40 | + value: bar 41 | + forcePull: true 42 | + noCache: true 43 | + type: Docker 44 | + successfulBuildsHistoryLimit: 5 45 | + triggers: [] 46 | 47 | 48 | Summary: 0 in sync, 1 to create, 0 to update, 0 to delete 49 | 50 | Creating bc/foo ... done 51 | -------------------------------------------------------------------------------- /internal/test/helper/file.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | "runtime" 9 | "testing" 10 | ) 11 | 12 | // SomeFilesExistFS is a mock filesystem where some files exist. 13 | type SomeFilesExistFS struct { 14 | Existing []string 15 | } 16 | 17 | // Stat always returns a nil error. 18 | func (fs SomeFilesExistFS) Stat(name string) (os.FileInfo, error) { 19 | for _, ef := range fs.Existing { 20 | if ef == name { 21 | return nil, nil 22 | } 23 | } 24 | return nil, os.ErrNotExist 25 | } 26 | 27 | // ReadFixtureFile returns the contents of the fixture file or fails. 28 | func ReadFixtureFile(t *testing.T, filename string) []byte { 29 | return readFileOrFatal(t, "../fixtures/"+filename) 30 | } 31 | 32 | // ReadGoldenFile returns the contents of the golden file or fails. 33 | func ReadGoldenFile(t *testing.T, filename string) []byte { 34 | return readFileOrFatal(t, "../golden/"+filename) 35 | } 36 | 37 | func readFileOrFatal(t *testing.T, name string) []byte { 38 | b, err := readFile(name) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | return b 43 | } 44 | 45 | func readFile(name string) ([]byte, error) { 46 | _, filename, _, ok := runtime.Caller(1) 47 | if !ok { 48 | return []byte{}, fmt.Errorf("Could not get filename when looking for %s", name) 49 | } 50 | filepath := path.Join(path.Dir(filename), name) 51 | return ioutil.ReadFile(filepath) 52 | } 53 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/recreate/2/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in recreate/2 with OCP namespace {{ .Project }}. 2 | Limiting resources to route/foo. 3 | Found 1 resource in OCP cluster (current state) and 1 resource in processed templates (desired state). 4 | 5 | - route/foo to delete 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1,18 +1 @@ 9 | -apiVersion: route.openshift.io/v1 10 | -kind: Route 11 | -metadata: 12 | - annotations: {} 13 | - labels: 14 | - app: foo-route 15 | - name: foo 16 | -spec: 17 | - host: foo.example.com 18 | - tls: 19 | - insecureEdgeTerminationPolicy: Redirect 20 | - termination: edge 21 | - to: 22 | - kind: Service 23 | - name: foo 24 | - weight: 100 25 | - wildcardPolicy: None 26 | 27 | + route/foo to create 28 | --- Current State (OpenShift cluster) 29 | +++ Desired State (Processed template) 30 | @@ -1 +1,17 @@ 31 | +apiVersion: route.openshift.io/v1 32 | +kind: Route 33 | +metadata: 34 | + labels: 35 | + app: foo-route 36 | + name: foo 37 | +spec: 38 | + host: foobar.example.com 39 | + tls: 40 | + insecureEdgeTerminationPolicy: Redirect 41 | + termination: edge 42 | + to: 43 | + kind: Service 44 | + name: foo 45 | + weight: 100 46 | + wildcardPolicy: None 47 | 48 | 49 | Summary: 0 in sync, 1 to create, 0 to update, 1 to delete 50 | 51 | Deleting route/foo ... done 52 | Creating route/foo ... done 53 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | version=$1 6 | 7 | if [ -z "$version" ]; then 8 | echo "No version passed! Example usage: ./release.sh 1.0.0" 9 | exit 1 10 | fi 11 | 12 | echo "Running tests..." 13 | make test 14 | 15 | echo "Update version..." 16 | grepped_version=$(grep -o "[0-9]*\.[0-9]*\.[0-9]+" cmd/tailor/main.go) 17 | old_version=${grepped_version%?} 18 | sed -i.bak 's/fmt.Println("'$old_version'+master")/fmt.Println("'$version'")/' cmd/tailor/main.go 19 | sed -i.bak 's/'$old_version'/'$version'/' README.md 20 | 21 | echo "Mark version as released in changelog..." 22 | today=$(date +'%Y-%m-%d') 23 | sed -i.bak 's/Unreleased/Unreleased\ 24 | \ 25 | ## ['$version'] - '$today'/' CHANGELOG.md 26 | 27 | rm *.bak 28 | 29 | echo "Build binaries..." 30 | make build 31 | 32 | echo "Update repository..." 33 | git add cmd/tailor/main.go README.md CHANGELOG.md 34 | git commit -m "Bump version to ${version}" 35 | git tag --message="v$version" --force "v$version" 36 | git tag --message="latest" --force latest 37 | 38 | echo "Set master version again" 39 | sed -i.bak 's/fmt.Println("'$version'")/fmt.Println("'$version'+master")/' cmd/tailor/main.go 40 | rm cmd/tailor/main.go.bak 41 | git add cmd/tailor/main.go 42 | git commit -m "Set master version to ${version}+master" 43 | 44 | echo "v$version tagged." 45 | echo "Now, run 'git push origin master && git push --tags --force' and publish the release on GitHub." 46 | -------------------------------------------------------------------------------- /internal/test/golden/desired-state/dc.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: DeploymentConfig 3 | metadata: 4 | labels: 5 | app: foo-bar 6 | name: bar 7 | spec: 8 | replicas: 1 9 | revisionHistoryLimit: 10 10 | selector: 11 | app: foo-bar 12 | strategy: 13 | activeDeadlineSeconds: 21600 14 | resources: {} 15 | rollingParams: 16 | intervalSeconds: 1 17 | maxSurge: 25% 18 | maxUnavailable: 25% 19 | timeoutSeconds: 600 20 | updatePeriodSeconds: 1 21 | type: Rolling 22 | template: 23 | metadata: 24 | labels: 25 | app: foo-bar 26 | spec: 27 | containers: 28 | - env: [] 29 | image: foo-test/bar:latest 30 | imagePullPolicy: IfNotPresent 31 | name: bar 32 | ports: 33 | - containerPort: 8080 34 | protocol: TCP 35 | resources: 36 | limits: 37 | memory: 2Gi 38 | requests: 39 | memory: 100Mi 40 | terminationMessagePath: /dev/termination-log 41 | terminationMessagePolicy: File 42 | dnsPolicy: ClusterFirst 43 | restartPolicy: Always 44 | schedulerName: default-scheduler 45 | securityContext: {} 46 | terminationGracePeriodSeconds: 30 47 | test: false 48 | triggers: 49 | - imageChangeParams: 50 | automatic: true 51 | containerNames: 52 | - bar 53 | from: 54 | kind: ImageStreamTag 55 | name: bar:latest 56 | namespace: foo-test 57 | type: ImageChange 58 | - type: ConfigChange 59 | -------------------------------------------------------------------------------- /pkg/cli/oc_version_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/opendevstack/tailor/internal/test/helper" 7 | ) 8 | 9 | type mockOcVersionClient struct { 10 | t *testing.T 11 | fixture string 12 | } 13 | 14 | func (c *mockOcVersionClient) Version() ([]byte, []byte, error) { 15 | content := helper.ReadFixtureFile(c.t, "version/"+c.fixture) 16 | return content, []byte(""), nil 17 | } 18 | 19 | func TestOcVersion(t *testing.T) { 20 | tests := map[string]struct { 21 | fixture string 22 | expectedClient string 23 | expectedServer string 24 | }{ 25 | "client=3.9 and server=3.11": { 26 | fixture: "client-3_9-and-server-3_11.txt", 27 | expectedClient: "v3.9", 28 | expectedServer: "v3.11", 29 | }, 30 | "client=3.11 and server=3.11": { 31 | fixture: "client-3_11-and-server-3_11.txt", 32 | expectedClient: "v3.11", 33 | expectedServer: "v3.11", 34 | }, 35 | "client=3.11 and server=?": { 36 | fixture: "client-3_11-and-server-unknown.txt", 37 | expectedClient: "v3.11", 38 | expectedServer: "?", 39 | }, 40 | } 41 | 42 | for name, tc := range tests { 43 | t.Run(name, func(t *testing.T) { 44 | c := &mockOcVersionClient{t: t, fixture: tc.fixture} 45 | ov := ocVersion(c) 46 | if ov.client != tc.expectedClient { 47 | t.Fatalf("Expected client version: '%s', got: '%s'", tc.expectedClient, ov.client) 48 | } 49 | if ov.server != tc.expectedServer { 50 | t.Fatalf("Expected client version: '%s', got: '%s'", tc.expectedServer, ov.server) 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /internal/test/golden/desired-state/dc-annotation.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: DeploymentConfig 3 | metadata: 4 | annotations: 5 | baz: qux 6 | labels: 7 | app: foo-bar 8 | name: bar 9 | spec: 10 | replicas: 1 11 | revisionHistoryLimit: 10 12 | selector: 13 | app: foo-bar 14 | strategy: 15 | activeDeadlineSeconds: 21600 16 | resources: {} 17 | rollingParams: 18 | intervalSeconds: 1 19 | maxSurge: 25% 20 | maxUnavailable: 25% 21 | timeoutSeconds: 600 22 | updatePeriodSeconds: 1 23 | type: Rolling 24 | template: 25 | metadata: 26 | labels: 27 | app: foo-bar 28 | spec: 29 | containers: 30 | - env: [] 31 | image: foo-test/bar:latest 32 | imagePullPolicy: IfNotPresent 33 | name: bar 34 | ports: 35 | - containerPort: 8080 36 | protocol: TCP 37 | resources: 38 | limits: 39 | memory: 2Gi 40 | requests: 41 | memory: 100Mi 42 | terminationMessagePath: /dev/termination-log 43 | terminationMessagePolicy: File 44 | dnsPolicy: ClusterFirst 45 | restartPolicy: Always 46 | schedulerName: default-scheduler 47 | securityContext: {} 48 | terminationGracePeriodSeconds: 30 49 | test: false 50 | triggers: 51 | - imageChangeParams: 52 | automatic: true 53 | containerNames: 54 | - bar 55 | from: 56 | kind: ImageStreamTag 57 | name: bar:latest 58 | namespace: foo-test 59 | type: ImageChange 60 | - type: ConfigChange 61 | -------------------------------------------------------------------------------- /pkg/openshift/test-encrypted.env: -------------------------------------------------------------------------------- 1 | FOO=wcFMAzyI1Y27MLXiARAAvObFAoJr3WmKHl/q4II+SKSVqVioVOECbxeKSgc0tjeKmoQbVXhZRxLoCP/FDeaf8WqgTfhIrImDz/2L0uJ7PU0ejlz4PtLJXiUOcbp7Z7985LUaB9QYeIQsFgbIOE/PWN1/TvNH7/j122+k9dCLeB82VSap+drulSzW02ypSSxiHeYaXE9rlHIvojcu0UvJfUuGBi0BspJnTe7P9OJL66B+VWstE3AsXe/23tJwLafmU0Vz4n6ylanpnasE61qCoXLiVk2GRIkBtxEFoh9uaa5GWbXXhJHnbA/Xn0Cxi8c8s54w1R9OzTywsuIH+Vto5YbfBYWcpesb3LpKHiISIxnHirSOIzRlBJdXEejsa/QM3t5qrs+3fspqHnVKqaYodgf51caaPqz1wl5VSe6OcoSa/G5FplsfFRGN8Jk/FkE2PRUtmaI+gWUNHbmzCQrpbxXKJORXPKs8uo1xrojqT9Ij6HSqpODk7ZxkevTz5f2yGIuHjtIn264ftZvMUnOPvihy84mE41hUVXvynpAJAPnDuVWyg42hlRDN3vSAJ8SpMPcOtozuCfi9mKvCtoV9bnoMGRVzHbTefetKdEjEiZMhgUHFN4BXEZDA0owtY4U/lRw1lHg0aqamoQuyxKKI65h0dttbgpbQguu+j3OjGyOXa9+ytd47/a90L5eW4cbS4AHklc1I9YIPDJY5EGhRUfKjduFfueC+4PfhUyng1eIBuG9C4CHi7jtSNuBO4RuV4Fbk5dPZ/5uXbL+mXuginPsRSOKds5Mh4SbaAA== 2 | BAR.B64=wcFMAzyI1Y27MLXiARAAO5vA8ZodlVc0rM2mKcu8vJGmvNbESAY5QfD5ZX98kctO6aGaSOHGiz21HTpE3zOA2qXM9qrK0K+ZCumSq47E1Cor1fwdGrNGu4krClkW0IiacbHx1GJPFmW9YWMeWr64Vqb974IAkb6Q1RezjYyyC2HoOg2qAmXcx7xw2a9sdYK9OE/9iE8WQC3pdkTm1JibDJyj30j6Ho0zTDEO4Y7dHYeabwKux5pN5O64p3i0unV0+s4tO5LJtEEndUyqzWTZWvcHyh8Jyzvyz+RgZNZe+zbCTFYDBDMfiW2aedvAbXvRLa93S88a8hnb1nkVN91TVuFFjl2PtC8bpzCEjyDIPYl+B8EPcvIJkberyOdSgCwRkwf4ASNghAOfTPnTAe81w0fPktnJYFx2glw7jqZ3TYfhsE+AfATrvct8rxikj0qZhhH0Hla7ZHxFSQrgWytsv2Y/A7VGniu7PXzoC7yd7YZjPz132RuZOEwr6gfzyWd2+tq+CfQq/Cx+CNRu9TBVkVbnYgM5vyjsAHoLAqrpB7X4ER5vlTkrRzq4eVKw0rBdTik7OLla1fZ2YistOeGVWpRb9xuCvsdamyHMPjW1B2WSvKoVKqnBVXPt2EX1X/DvPw9gsvYvr7wXIZ7QEUVMVQbpi9c6EVbs5opiFOZR7v0Lj65CPt7CA2wx6UtRhBbS4AHkY9/qrK4+zJtvl70uqiCZKeFS4eDz4O/h0lDgj+J4KaZB4FbjXmqE4Gm1BR/gNuRMvIqag/RGZZCyxjO+1gKQ4r5njYLhB8AA 3 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/selector/steps.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "apply -l app=foo", 4 | "wantStdout": true, 5 | "wantResources": { 6 | "cm/foo": true, 7 | "svc/foo": true, 8 | "cm/bar": false, 9 | "svc/bar": false 10 | }, 11 | "wantFields": { 12 | "cm/foo": { 13 | ".data.bar": "baz" 14 | }, 15 | "svc/foo": { 16 | ".spec.selector.name": "foo" 17 | } 18 | } 19 | }, 20 | { 21 | "command": "apply -l app=bar", 22 | "wantStdout": true, 23 | "wantResources": { 24 | "cm/foo": true, 25 | "svc/foo": true, 26 | "cm/bar": true, 27 | "svc/bar": true 28 | }, 29 | "wantFields": { 30 | "cm/bar": { 31 | ".data.bar": "baz" 32 | }, 33 | "svc/bar": { 34 | ".spec.selector.name": "bar" 35 | } 36 | } 37 | }, 38 | { 39 | "command": "diff -l app=foo", 40 | "wantStdout": true, 41 | "wantErr": false, 42 | "wantResources": { 43 | "cm/foo": true, 44 | "svc/foo": true, 45 | "cm/bar": true, 46 | "svc/bar": true 47 | } 48 | }, 49 | { 50 | "command": "diff -l app=bar", 51 | "wantStdout": true, 52 | "wantErr": true, 53 | "wantResources": { 54 | "cm/foo": true, 55 | "svc/foo": true, 56 | "cm/bar": true, 57 | "svc/bar": true 58 | } 59 | } 60 | ] 61 | -------------------------------------------------------------------------------- /internal/test/fixtures/templates/dc.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | items: 4 | - apiVersion: v1 5 | kind: DeploymentConfig 6 | metadata: 7 | labels: 8 | app: foo-bar 9 | name: bar 10 | spec: 11 | revisionHistoryLimit: 10 12 | replicas: 1 13 | selector: 14 | app: foo-bar 15 | strategy: 16 | activeDeadlineSeconds: 21600 17 | resources: {} 18 | rollingParams: 19 | intervalSeconds: 1 20 | maxSurge: 25% 21 | maxUnavailable: 25% 22 | timeoutSeconds: 600 23 | updatePeriodSeconds: 1 24 | type: Rolling 25 | template: 26 | metadata: 27 | creationTimestamp: null 28 | labels: 29 | app: foo-bar 30 | spec: 31 | containers: 32 | - image: foo-test/bar:latest 33 | imagePullPolicy: IfNotPresent 34 | name: bar 35 | env: [] 36 | ports: 37 | - containerPort: 8080 38 | protocol: TCP 39 | resources: 40 | limits: 41 | memory: 2Gi 42 | requests: 43 | memory: 100Mi 44 | terminationMessagePath: /dev/termination-log 45 | terminationMessagePolicy: File 46 | dnsPolicy: ClusterFirst 47 | restartPolicy: Always 48 | schedulerName: default-scheduler 49 | securityContext: {} 50 | terminationGracePeriodSeconds: 30 51 | test: false 52 | triggers: 53 | - type: ImageChange 54 | imageChangeParams: 55 | automatic: true 56 | containerNames: 57 | - bar 58 | from: 59 | kind: ImageStreamTag 60 | name: bar:latest 61 | namespace: foo-test 62 | - type: ConfigChange 63 | -------------------------------------------------------------------------------- /internal/test/fixtures/templates/dc-annotation.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | items: 4 | - apiVersion: v1 5 | kind: DeploymentConfig 6 | metadata: 7 | annotations: 8 | baz: qux 9 | labels: 10 | app: foo-bar 11 | name: bar 12 | spec: 13 | revisionHistoryLimit: 10 14 | replicas: 1 15 | selector: 16 | app: foo-bar 17 | strategy: 18 | activeDeadlineSeconds: 21600 19 | resources: {} 20 | rollingParams: 21 | intervalSeconds: 1 22 | maxSurge: 25% 23 | maxUnavailable: 25% 24 | timeoutSeconds: 600 25 | updatePeriodSeconds: 1 26 | type: Rolling 27 | template: 28 | metadata: 29 | creationTimestamp: null 30 | labels: 31 | app: foo-bar 32 | spec: 33 | containers: 34 | - image: foo-test/bar:latest 35 | imagePullPolicy: IfNotPresent 36 | name: bar 37 | env: [] 38 | ports: 39 | - containerPort: 8080 40 | protocol: TCP 41 | resources: 42 | limits: 43 | memory: 2Gi 44 | requests: 45 | memory: 100Mi 46 | terminationMessagePath: /dev/termination-log 47 | terminationMessagePolicy: File 48 | dnsPolicy: ClusterFirst 49 | restartPolicy: Always 50 | schedulerName: default-scheduler 51 | securityContext: {} 52 | terminationGracePeriodSeconds: 30 53 | test: false 54 | triggers: 55 | - type: ImageChange 56 | imageChangeParams: 57 | automatic: true 58 | containerNames: 59 | - bar 60 | from: 61 | kind: ImageStreamTag 62 | name: bar:latest 63 | namespace: foo-test 64 | - type: ConfigChange 65 | -------------------------------------------------------------------------------- /pkg/cli/cli_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "testing" 7 | ) 8 | 9 | func TestAskForAction(t *testing.T) { 10 | tests := map[string]struct { 11 | input string 12 | options []string 13 | expectedAnswer string 14 | }{ 15 | "input of key of one option": { 16 | input: "y\n", 17 | options: []string{"y=yes", "n=no"}, 18 | expectedAnswer: "y", 19 | }, 20 | "input of value of one option": { 21 | input: "yes\n", 22 | options: []string{"y=yes", "n=no"}, 23 | expectedAnswer: "y", 24 | }, 25 | "input of key with uppercase character": { 26 | input: "Y\n", 27 | options: []string{"y=yes", "n=no"}, 28 | expectedAnswer: "y", 29 | }, 30 | "input of value with uppercase character": { 31 | input: "Yes\n", 32 | options: []string{"y=yes", "n=no"}, 33 | expectedAnswer: "y", 34 | }, 35 | "input of invalid answer at first": { 36 | input: "m\nn\n", 37 | options: []string{"y=yes", "n=no"}, 38 | expectedAnswer: "n", 39 | }, 40 | "empty input at first": { 41 | input: "\ny\n", 42 | options: []string{"y=yes", "n=no"}, 43 | expectedAnswer: "y", 44 | }, 45 | "input with surrounding space": { 46 | input: " no \n", 47 | options: []string{"y=yes", "n=no"}, 48 | expectedAnswer: "n", 49 | }, 50 | } 51 | 52 | for name, tc := range tests { 53 | t.Run(name, func(t *testing.T) { 54 | var stdin bytes.Buffer 55 | stdin.Write([]byte(tc.input)) 56 | stdinReader := bufio.NewReader(&stdin) 57 | a := AskForAction("What?", tc.options, stdinReader) 58 | if a != tc.expectedAnswer { 59 | t.Fatalf("Want: '%s', got: '%s'", tc.expectedAnswer, a) 60 | } 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning - action" 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 5 * * 4' 8 | 9 | jobs: 10 | CodeQL-Build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v2 17 | with: 18 | # We must fetch at least the immediate parents so that if this is 19 | # a pull request then we can checkout the head. 20 | fetch-depth: 2 21 | 22 | # If this run was triggered by a pull request event, then checkout 23 | # the head of the pull request instead of the merge commit. 24 | - run: git checkout HEAD^2 25 | if: ${{ github.event_name == 'pull_request' }} 26 | 27 | # Initializes the CodeQL tools for scanning. 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@v1 30 | # Override language selection by uncommenting this and choosing your languages 31 | # with: 32 | # languages: go, javascript, csharp, python, cpp, java 33 | 34 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 35 | # If this step fails, then you should remove it and run the build manually (see below) 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v1 38 | 39 | # ℹ️ Command-line programs to run using the OS shell. 40 | # 📚 https://git.io/JvXDl 41 | 42 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 43 | # and modify them (or add more) to build your code if your project 44 | # uses a compiled language 45 | 46 | #- run: | 47 | # make bootstrap 48 | # make release 49 | 50 | - name: Perform CodeQL Analysis 51 | uses: github/codeql-action/analyze@v1 52 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/deploymentconfig/steps.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "apply dc,secret", 4 | "wantStdout": true, 5 | "wantResources": { 6 | "dc/foo": true 7 | }, 8 | "wantFields": { 9 | "dc/foo": { 10 | ".metadata.name": "foo", 11 | ".spec.template.spec.containers[0].env[0].name": "FOO", 12 | ".spec.template.spec.containers[0].env[0].value": "abc", 13 | ".spec.template.spec.containers[0].env[1].name": "QUX", 14 | ".spec.template.spec.containers[0].env[1].valueFrom.secretKeyRef.name": "foo-user", 15 | ".spec.template.spec.containers[0].env[2].name": "BAZ", 16 | ".spec.template.spec.containers[0].env[2].value": "http://baz.{{ .Project }}.svc:8080/" 17 | } 18 | } 19 | }, 20 | { 21 | "command": "apply dc,secret", 22 | "wantStdout": true, 23 | "wantResources": { 24 | "dc/foo": true 25 | }, 26 | "wantFields": { 27 | "dc/foo": { 28 | ".metadata.labels.app": "foo", 29 | ".spec.template.spec.containers[0].image": "docker-registry.default.svc:5000/{{ .Project }}/foo:latest", 30 | ".spec.template.spec.containers[0].env[0].name": "FOO", 31 | ".spec.template.spec.containers[0].env[0].value": "abc", 32 | ".spec.template.spec.containers[0].env[1].name": "BAZ", 33 | ".spec.template.spec.containers[0].env[1].value": "http://baz.{{ .Project }}.svc:8080/" 34 | } 35 | } 36 | }, 37 | { 38 | "command": "apply dc,secret", 39 | "wantStdout": true, 40 | "wantErr": true, 41 | "wantResources": { 42 | "dc/foo": true 43 | } 44 | }, 45 | { 46 | "command": "apply dc,secret --force", 47 | "wantResources": { 48 | "dc/foo": false 49 | } 50 | } 51 | ] 52 | -------------------------------------------------------------------------------- /internal/test/fixtures/command-apply/current-list.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | items: 3 | - apiVersion: build.openshift.io/v1 4 | kind: BuildConfig 5 | metadata: 6 | creationTimestamp: "2020-05-05T11:25:28Z" 7 | name: foo 8 | namespace: ods 9 | resourceVersion: "375626209" 10 | selfLink: /apis/build.openshift.io/v1/namespaces/ods/buildconfigs/foo 11 | uid: 1fafe5ac-8ec3-11ea-a8a2-0aad3153d0e6 12 | spec: 13 | failedBuildsHistoryLimit: 5 14 | nodeSelector: null 15 | output: 16 | to: 17 | kind: ImageStreamTag 18 | name: foo:latest 19 | postCommit: {} 20 | resources: 21 | limits: 22 | cpu: "1" 23 | memory: 1Gi 24 | requests: 25 | cpu: 200m 26 | memory: 512Mi 27 | runPolicy: Serial 28 | source: 29 | git: 30 | ref: master 31 | uri: https://example.com/example/foo.git 32 | sourceSecret: 33 | name: user-credentials 34 | type: Git 35 | strategy: 36 | dockerStrategy: {} 37 | type: Docker 38 | successfulBuildsHistoryLimit: 5 39 | triggers: [] 40 | status: 41 | lastVersion: 3 42 | - apiVersion: image.openshift.io/v1 43 | kind: ImageStream 44 | metadata: 45 | creationTimestamp: "2020-05-05T11:25:26Z" 46 | generation: 1 47 | name: foo 48 | namespace: ods 49 | resourceVersion: "385616336" 50 | selfLink: /apis/image.openshift.io/v1/namespaces/ods/imagestreams/foo 51 | uid: 1e735d57-8ec3-11ea-94ce-0a30b7cbe558 52 | spec: 53 | dockerImageRepository: foo 54 | lookupPolicy: 55 | local: false 56 | status: 57 | dockerImageRepository: 172.30.21.196:5000/ods/foo 58 | tags: 59 | - items: 60 | - created: "2020-05-15T12:21:25Z" 61 | dockerImageReference: 172.30.21.196:5000/ods/foo@sha256:a15e0927a9f50c162b20e5632119e785977f364b1558c1e9d666caaab595307a 62 | generation: 1 63 | image: sha256:a15e0927a9f50c162b20e5632119e785977f364b1558c1e9d666caaab595307a 64 | tag: latest 65 | kind: List 66 | metadata: 67 | resourceVersion: "" 68 | selfLink: "" 69 | -------------------------------------------------------------------------------- /pkg/cli/oc_version.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // openshiftVersion represents the client/server version pair. 8 | type openshiftVersion struct { 9 | client string 10 | server string 11 | } 12 | 13 | // ExactMatch is true when client and server version are known and equal. 14 | func (ov openshiftVersion) ExactMatch() bool { 15 | return !ov.Incomplete() && ov.client == ov.server 16 | } 17 | 18 | // Incomplete returns true if at least one version could not be detected properly. 19 | func (ov openshiftVersion) Incomplete() bool { 20 | return ov.client == "?" || ov.server == "?" 21 | } 22 | 23 | // Get OC client and server version. See tests for example output of "oc version". 24 | func ocVersion(ocClient OcClientVersioner) openshiftVersion { 25 | ov := openshiftVersion{"?", "?"} 26 | outBytes, errBytes, err := ocClient.Version() 27 | if err != nil { 28 | VerboseMsg("Failed to query client and server version, got:", string(errBytes)) 29 | return ov 30 | } 31 | output := string(outBytes) 32 | 33 | ocClientVersion := "" 34 | ocServerVersion := "" 35 | extractVersion := func(versionPart string) string { 36 | ocVersionParts := strings.SplitN(versionPart, ".", 3) 37 | return strings.Join(ocVersionParts[:len(ocVersionParts)-1], ".") 38 | } 39 | 40 | lines := strings.Split(strings.TrimSuffix(output, "\n"), "\n") 41 | for _, line := range lines { 42 | if len(line) > 0 { 43 | parts := strings.SplitN(line, " ", 2) 44 | if parts[0] == "oc" { 45 | ocClientVersion = extractVersion(parts[1]) 46 | } 47 | if parts[0] == "openshift" { 48 | ocServerVersion = extractVersion(parts[1]) 49 | } 50 | } 51 | } 52 | 53 | if len(ocClientVersion) > 0 && strings.Contains(ocClientVersion, ".") { 54 | ov.client = ocClientVersion 55 | } 56 | if len(ocServerVersion) > 0 && strings.Contains(ocServerVersion, ".") { 57 | ov.server = ocServerVersion 58 | } 59 | 60 | if ov.Incomplete() { 61 | VerboseMsg("Client and server version could not be detected properly, got:", output) 62 | return ov 63 | } 64 | return openshiftVersion{client: ocClientVersion, server: ocServerVersion} 65 | } 66 | -------------------------------------------------------------------------------- /internal/test/e2e/e2e_helper.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "math/rand" 5 | "os" 6 | "os/exec" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | const charset = "abcdefghijklmnopqrstuvwxyz" 12 | 13 | var seededRand *rand.Rand = rand.New( 14 | rand.NewSource(time.Now().UnixNano())) 15 | 16 | func randomString(length int) string { 17 | b := make([]byte, length) 18 | for i := range b { 19 | b[i] = charset[seededRand.Intn(len(charset))] 20 | } 21 | return string(b) 22 | } 23 | 24 | func setup(t *testing.T) string { 25 | t.Log("SETUP: Checking for local cluster ...") 26 | cmd := exec.Command("oc", []string{"whoami"}...) 27 | _, err := cmd.CombinedOutput() 28 | if err == nil { 29 | t.Log("SETUP: Local cluster running ...") 30 | } else if os.Getenv("LAUNCH_LOCAL_CLUSTER") == "yes" { 31 | launchLocalCluster(t) 32 | } 33 | return makeTestProject(t) 34 | } 35 | 36 | func teardown(t *testing.T, project string) { 37 | deleteTestProject(t, project) 38 | } 39 | 40 | func getTailorBinary() string { 41 | dir, _ := os.Getwd() 42 | return dir + "/../tailor-test" 43 | } 44 | 45 | func launchLocalCluster(t *testing.T) { 46 | t.Log("SETUP: Launching local cluster ...") 47 | cmd := exec.Command("oc", []string{"cluster", "up"}...) 48 | out, err := cmd.CombinedOutput() 49 | if err != nil { 50 | t.Fatalf("SETUP: Could not launch local cluster: %s", out) 51 | } 52 | t.Log("SETUP: Local cluster launched") 53 | } 54 | 55 | func deleteTestProject(t *testing.T, project string) { 56 | cmd := exec.Command("oc", []string{"delete", "project", project}...) 57 | out, err := cmd.CombinedOutput() 58 | if err != nil { 59 | t.Fatalf("TEARDOWN: Could not delete project %s: %s", project, out) 60 | } 61 | t.Log("TEARDOWN:", project, "project deleted") 62 | } 63 | 64 | func makeTestProject(t *testing.T) string { 65 | project := "tailor-e2e-test-" + randomString(6) 66 | cmd := exec.Command("oc", []string{"new-project", project}...) 67 | out, err := cmd.CombinedOutput() 68 | if err != nil { 69 | t.Fatalf("SETUP: Could not create project %s: %s", project, out) 70 | } 71 | t.Log("SETUP: Project", project, "created") 72 | return project 73 | } 74 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/job/3/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in job/3 with OCP namespace {{ .Project }}. 2 | Limiting resources to job. 3 | Found 1 resource in OCP cluster (current state) and 0 resources in processed templates (desired state). 4 | 5 | - job/pi to delete 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1,40 +1 @@ 9 | -apiVersion: batch/v1 10 | -kind: Job 11 | -metadata: 12 | - annotations: 13 | - kubectl.kubernetes.io/last-applied-configuration: | 14 | - {"apiVersion":"batch/v1","kind":"Job","metadata":{"annotations":{},"labels":{"job-name":"pi"},"name":"pi","namespace":"{{ .Project }}"},"spec":{"backoffLimit":5,"completions":1,"parallelism":1,"selector":{"matchLabels":{"job-name":"pi"}},"template":{"metadata":{"labels":{"job-name":"pi"},"name":"pi"},"spec":{"containers":[{"command":["perl","-Mbignum=bpi","-wle","print bpi(2000)"],"image":"perl","imagePullPolicy":"Always","name":"pi","resources":{},"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File"}],"dnsPolicy":"ClusterFirst","restartPolicy":"OnFailure","schedulerName":"default-scheduler","securityContext":{},"terminationGracePeriodSeconds":30}}}} 15 | - labels: 16 | - job-name: pi 17 | - name: pi 18 | -spec: 19 | - backoffLimit: 5 20 | - completions: 1 21 | - parallelism: 1 22 | - selector: 23 | - matchLabels: 24 | - job-name: pi 25 | - template: 26 | - metadata: 27 | - labels: 28 | - job-name: pi 29 | - name: pi 30 | - spec: 31 | - containers: 32 | - - command: 33 | - - perl 34 | - - -Mbignum=bpi 35 | - - -wle 36 | - - print bpi(2000) 37 | - image: perl 38 | - imagePullPolicy: Always 39 | - name: pi 40 | - resources: {} 41 | - terminationMessagePath: /dev/termination-log 42 | - terminationMessagePolicy: File 43 | - dnsPolicy: ClusterFirst 44 | - restartPolicy: OnFailure 45 | - schedulerName: default-scheduler 46 | - securityContext: {} 47 | - terminationGracePeriodSeconds: 30 48 | 49 | 50 | Summary: 0 in sync, 0 to create, 0 to update, 1 to delete 51 | 52 | Deleting job/pi ... done 53 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/buildconfig/3/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in buildconfig/3 with OCP namespace {{ .Project }}. 2 | Limiting resources to bc. 3 | Found 1 resource in OCP cluster (current state) and 0 resources in processed templates (desired state). 4 | 5 | - bc/foo to delete 6 | --- Current State (OpenShift cluster) 7 | +++ Desired State (Processed template) 8 | @@ -1,42 +1 @@ 9 | -apiVersion: build.openshift.io/v1 10 | -kind: BuildConfig 11 | -metadata: 12 | - annotations: 13 | - kubectl.kubernetes.io/last-applied-configuration: | 14 | - {"apiVersion":"build.openshift.io/v1","kind":"BuildConfig","metadata":{"annotations":{},"name":"foo","namespace":"{{ .Project }}"},"spec":{"failedBuildsHistoryLimit":5,"nodeSelector":null,"output":{"to":{"kind":"ImageStreamTag","name":"foo:latest"}},"postCommit":{},"resources":{"limits":{"cpu":"1","memory":"256Mi"},"requests":{"cpu":"500m","memory":"128Mi"}},"runPolicy":"Serial","source":{"contextDir":"baz","git":{"ref":"master","uri":"https://github.com/opendevstack/tailor.git"},"sourceSecret":{"name":"token"},"type":"Git"},"strategy":{"dockerStrategy":{"buildArgs":[{"name":"foo","value":"bar"}],"forcePull":true,"noCache":true},"type":"Docker"},"successfulBuildsHistoryLimit":5,"triggers":[]}} 15 | - name: foo 16 | -spec: 17 | - failedBuildsHistoryLimit: 5 18 | - nodeSelector: null 19 | - output: 20 | - to: 21 | - kind: ImageStreamTag 22 | - name: foo:latest 23 | - postCommit: {} 24 | - resources: 25 | - limits: 26 | - cpu: "1" 27 | - memory: 256Mi 28 | - requests: 29 | - cpu: 500m 30 | - memory: 128Mi 31 | - runPolicy: Serial 32 | - source: 33 | - contextDir: baz 34 | - git: 35 | - ref: master 36 | - uri: https://github.com/opendevstack/tailor.git 37 | - sourceSecret: 38 | - name: token 39 | - type: Git 40 | - strategy: 41 | - dockerStrategy: 42 | - buildArgs: 43 | - - name: foo 44 | - value: bar 45 | - forcePull: true 46 | - noCache: true 47 | - type: Docker 48 | - successfulBuildsHistoryLimit: 5 49 | - triggers: [] 50 | 51 | 52 | Summary: 0 in sync, 0 to create, 0 to update, 1 to delete 53 | 54 | Deleting bc/foo ... done 55 | -------------------------------------------------------------------------------- /pkg/openshift/params_test.go: -------------------------------------------------------------------------------- 1 | package openshift 2 | 3 | import ( 4 | "io/ioutil" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestDecryptedParams(t *testing.T) { 10 | input := readFileContent(t, "test-encrypted.env") 11 | t.Logf("Read input: %s", input) 12 | expected := readFileContent(t, "test-cleartext.env") 13 | t.Logf("Read expected: %s", expected) 14 | actual, err := DecryptedParams(input, "test-private.key", "") 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | if actual != expected { 19 | t.Errorf("Mismatch, got: %v, want: %v.", actual, expected) 20 | } 21 | } 22 | 23 | func TestEncodedParams(t *testing.T) { 24 | input := readFileContent(t, "test-encrypted.env") 25 | t.Logf("Read input: %s", input) 26 | expected := readFileContent(t, "test-encoded.env") 27 | t.Logf("Read expected: %s", expected) 28 | actual, err := EncodedParams(input, "test-private.key", "") 29 | if err != nil { 30 | t.Error(err) 31 | } 32 | if actual != expected { 33 | t.Errorf("Mismatch, got: %v, want: %v.", actual, expected) 34 | } 35 | } 36 | 37 | func TestEncryptedParams(t *testing.T) { 38 | previous := readFileContent(t, "test-encrypted.env") 39 | t.Logf("Read previous: %s", previous) 40 | input := readFileContent(t, "test-cleartext.env") 41 | // Add one additional line ... 42 | input = input + "BAZ=baz\n" 43 | t.Logf("Read input: %s", input) 44 | actual, err := EncryptedParams(input, previous, ".", "test-private.key", "") 45 | if err != nil { 46 | t.Error(err) 47 | } 48 | // The expected output is the first line of the previous file 49 | // plus one additional line (added above) 50 | expectedText := strings.TrimSuffix(previous, "\n") 51 | expectedLines := strings.Split(expectedText, "\n") 52 | actualText := strings.TrimSuffix(actual, "\n") 53 | actualLines := strings.Split(actualText, "\n") 54 | if actualLines[0] != expectedLines[0] { 55 | t.Errorf("Mismatch, got: %v, want: %v.", actualLines[0], expectedLines[0]) 56 | } 57 | if actualLines[1] != expectedLines[1] { 58 | t.Errorf("Mismatch, got: %v, want: %v.", actualLines[1], expectedLines[1]) 59 | } 60 | if !strings.HasPrefix(actualLines[2], "BAZ=") { 61 | t.Errorf("Mismatch, got: %v, want: %v.", actualLines[2], "BAZ=") 62 | } 63 | } 64 | 65 | func readFileContent(t *testing.T, filename string) string { 66 | bytes, err := ioutil.ReadFile(filename) 67 | if err != nil { 68 | t.Error(err) 69 | } 70 | return string(bytes) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/openshift/list.go: -------------------------------------------------------------------------------- 1 | package openshift 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/ghodss/yaml" 7 | "github.com/opendevstack/tailor/pkg/cli" 8 | "github.com/opendevstack/tailor/pkg/utils" 9 | "github.com/xeipuuv/gojsonpointer" 10 | ) 11 | 12 | // ResourceList is a collection of resources that conform to a filter. 13 | type ResourceList struct { 14 | Filter *ResourceFilter 15 | Items []*ResourceItem 16 | } 17 | 18 | // NewTemplateBasedResourceList assembles a ResourceList from an input that is 19 | // treated as coming from a local template (desired state). 20 | func NewTemplateBasedResourceList(filter *ResourceFilter, inputs ...[]byte) (*ResourceList, error) { 21 | list := &ResourceList{Filter: filter} 22 | err := list.appendItems("template", "/items", inputs...) 23 | return list, err 24 | } 25 | 26 | // NewPlatformBasedResourceList assembles a ResourceList from an input that is 27 | // treated as coming from an OpenShift cluster (current state). 28 | func NewPlatformBasedResourceList(filter *ResourceFilter, inputs ...[]byte) (*ResourceList, error) { 29 | list := &ResourceList{Filter: filter} 30 | err := list.appendItems("platform", "/items", inputs...) 31 | return list, err 32 | } 33 | 34 | // Length returns the number of items in the resource list 35 | func (l *ResourceList) Length() int { 36 | return len(l.Items) 37 | } 38 | 39 | func (l *ResourceList) getItem(kind string, name string) (*ResourceItem, error) { 40 | for _, item := range l.Items { 41 | if item.Kind == kind && item.Name == name { 42 | return item, nil 43 | } 44 | } 45 | return nil, errors.New("No such item") 46 | } 47 | 48 | func (l *ResourceList) appendItems(source, itemsField string, inputs ...[]byte) error { 49 | for _, input := range inputs { 50 | if len(input) == 0 { 51 | cli.DebugMsg("Input config empty") 52 | continue 53 | } 54 | 55 | var f interface{} 56 | err := yaml.Unmarshal(input, &f) 57 | if err != nil { 58 | err = utils.DisplaySyntaxError(input, err) 59 | return err 60 | } 61 | m := f.(map[string]interface{}) 62 | 63 | p, _ := gojsonpointer.NewJsonPointer(itemsField) 64 | items, _, err := p.Get(m) 65 | if err != nil { 66 | return err 67 | } 68 | if items == nil { 69 | return errors.New("Cannot find items to append") 70 | } 71 | for _, v := range items.([]interface{}) { 72 | item, err := NewResourceItem(v.(map[string]interface{}), source) 73 | if err != nil { 74 | return err 75 | } 76 | if item.Comparable && l.Filter.SatisfiedBy(item) { 77 | l.Items = append(l.Items, item) 78 | } 79 | } 80 | } 81 | 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/deploymentconfig/1/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | parameters: 4 | - name: TAILOR_NAMESPACE 5 | required: true 6 | objects: 7 | - apiVersion: v1 8 | kind: Secret 9 | metadata: 10 | name: foo-user 11 | type: kubernetes.io/basic-auth 12 | data: 13 | password: c2VjcmV0 14 | username: dXNlcg== 15 | - apiVersion: apps.openshift.io/v1 16 | kind: DeploymentConfig 17 | metadata: 18 | labels: 19 | app: foo 20 | name: foo 21 | spec: 22 | replicas: 1 23 | revisionHistoryLimit: 10 24 | selector: 25 | app: foo 26 | strategy: 27 | activeDeadlineSeconds: 21600 28 | resources: {} 29 | rollingParams: 30 | intervalSeconds: 1 31 | maxSurge: 25% 32 | maxUnavailable: 25% 33 | timeoutSeconds: 600 34 | updatePeriodSeconds: 1 35 | type: Rolling 36 | template: 37 | metadata: 38 | labels: 39 | app: foo 40 | spec: 41 | containers: 42 | - env: 43 | - name: FOO 44 | value: abc 45 | - name: BAZ 46 | value: http://baz.${TAILOR_NAMESPACE}.svc:8080/ 47 | image: docker-registry.default.svc:5000/${TAILOR_NAMESPACE}/foo:latest 48 | imagePullPolicy: Always 49 | livenessProbe: 50 | failureThreshold: 3 51 | httpGet: 52 | path: "/health" 53 | port: 8080 54 | scheme: HTTP 55 | initialDelaySeconds: 6 56 | periodSeconds: 10 57 | successThreshold: 1 58 | timeoutSeconds: 3 59 | name: foo 60 | ports: 61 | - containerPort: 8080 62 | protocol: TCP 63 | readinessProbe: 64 | failureThreshold: 3 65 | httpGet: 66 | path: "/health" 67 | port: 8080 68 | scheme: HTTP 69 | initialDelaySeconds: 3 70 | periodSeconds: 10 71 | successThreshold: 1 72 | timeoutSeconds: 3 73 | resources: 74 | limits: 75 | cpu: 100m 76 | memory: 128Mi 77 | requests: 78 | cpu: 50m 79 | memory: 128Mi 80 | terminationMessagePath: /dev/termination-log 81 | terminationMessagePolicy: File 82 | dnsPolicy: ClusterFirst 83 | restartPolicy: Always 84 | schedulerName: default-scheduler 85 | securityContext: {} 86 | terminationGracePeriodSeconds: 30 87 | test: false 88 | triggers: 89 | - type: ConfigChange 90 | -------------------------------------------------------------------------------- /pkg/openshift/item_test.go: -------------------------------------------------------------------------------- 1 | package openshift 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/ghodss/yaml" 8 | "github.com/opendevstack/tailor/internal/test/helper" 9 | ) 10 | 11 | func TestNewResourceItem(t *testing.T) { 12 | item := getItem(t, getBuildConfig(), "template") 13 | if item.Kind != "BuildConfig" { 14 | t.Errorf("Kind is %s but should be BuildConfig", item.Kind) 15 | } 16 | if item.Name != "foo" { 17 | t.Errorf("Name is %s but should be foo", item.Name) 18 | } 19 | if item.Labels["app"] != "foo" { 20 | t.Errorf("Label app is %s but should be foo", item.Labels["app"]) 21 | } 22 | } 23 | 24 | func getPlatformItem(t *testing.T, filename string) *ResourceItem { 25 | return getItem(t, helper.ReadFixtureFile(t, filename), "platform") 26 | } 27 | 28 | func getTemplateItem(t *testing.T, filename string) *ResourceItem { 29 | return getItem(t, helper.ReadFixtureFile(t, filename), "template") 30 | } 31 | 32 | func getItem(t *testing.T, input []byte, source string) *ResourceItem { 33 | var f interface{} 34 | err := yaml.Unmarshal(input, &f) 35 | if err != nil { 36 | t.Fatalf("Could not umarshal yaml: %v", err) 37 | } 38 | m := f.(map[string]interface{}) 39 | item, err := NewResourceItem(m, source) 40 | if err != nil { 41 | t.Errorf("Could not create item: %v", err) 42 | } 43 | return item 44 | } 45 | 46 | func getBuildConfig() []byte { 47 | return []byte( 48 | `apiVersion: v1 49 | kind: BuildConfig 50 | metadata: 51 | annotations: {} 52 | labels: 53 | app: foo 54 | name: foo 55 | spec: 56 | failedBuildsHistoryLimit: 5 57 | nodeSelector: null 58 | output: 59 | to: 60 | kind: ImageStreamTag 61 | name: foo:latest 62 | postCommit: {} 63 | resources: {} 64 | runPolicy: Serial 65 | source: 66 | binary: {} 67 | type: Binary 68 | strategy: 69 | dockerStrategy: {} 70 | type: Docker 71 | successfulBuildsHistoryLimit: 5 72 | triggers: 73 | - generic: 74 | secret: password 75 | type: Generic 76 | - imageChange: {} 77 | type: ImageChange 78 | - type: ConfigChange`) 79 | } 80 | 81 | func getRoute(host []byte) []byte { 82 | config := []byte( 83 | `apiVersion: v1 84 | kind: Route 85 | metadata: 86 | annotations: {} 87 | name: foo 88 | spec: 89 | host: HOST 90 | tls: 91 | insecureEdgeTerminationPolicy: Redirect 92 | termination: edge 93 | to: 94 | kind: Service 95 | name: foo 96 | weight: 100 97 | wildcardPolicy: None`) 98 | 99 | return bytes.Replace(config, []byte("HOST"), host, -1) 100 | } 101 | -------------------------------------------------------------------------------- /pkg/openshift/export.go: -------------------------------------------------------------------------------- 1 | package openshift 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/ghodss/yaml" 10 | "github.com/opendevstack/tailor/pkg/cli" 11 | ) 12 | 13 | var ( 14 | trimAnnotationsDefault = []string{ 15 | "kubectl.kubernetes.io/last-applied-configuration", 16 | "openshift.io/image.dockerRepositoryCheck", 17 | } 18 | ) 19 | 20 | // ExportAsTemplateFile exports resources in template format. 21 | func ExportAsTemplateFile(filter *ResourceFilter, withAnnotations bool, namespace string, withHardcodedNamespace bool, trimAnnotations []string, ocClient cli.OcClientExporter) (string, error) { 22 | outBytes, err := ocClient.Export(filter.ConvertToKinds(), filter.Label) 23 | if err != nil { 24 | return "", fmt.Errorf("Could not export %s resources: %s", filter.String(), err) 25 | } 26 | if len(outBytes) == 0 { 27 | return "", nil 28 | } 29 | 30 | if !withHardcodedNamespace { 31 | namespaceRegex := regexp.MustCompile(`\b` + namespace + `\b.?`) 32 | outBytes = namespaceRegex.ReplaceAllFunc(outBytes, func(b []byte) []byte { 33 | if bytes.HasSuffix(b, []byte("-")) { 34 | return b 35 | } 36 | return bytes.Replace(b, []byte(namespace), []byte("${TAILOR_NAMESPACE}"), -1) 37 | }) 38 | } 39 | 40 | list, err := NewPlatformBasedResourceList(filter, outBytes) 41 | if err != nil { 42 | return "", fmt.Errorf("Could not create resource list from export: %s", err) 43 | } 44 | 45 | objects := []map[string]interface{}{} 46 | for _, i := range list.Items { 47 | if withAnnotations { 48 | cli.DebugMsg("All annotations will be kept in template item") 49 | } else { 50 | trimAnnotations = append(trimAnnotations, trimAnnotationsDefault...) 51 | cli.DebugMsg("Trim annotations from template item") 52 | for ia := range i.Annotations { 53 | for _, ta := range trimAnnotations { 54 | if strings.HasSuffix(ta, "/") && strings.HasPrefix(ia, ta) { 55 | i.removeAnnotion(ia) 56 | } else if ta == ia { 57 | i.removeAnnotion(ia) 58 | } 59 | } 60 | } 61 | } 62 | objects = append(objects, i.Config) 63 | } 64 | 65 | t := map[string]interface{}{ 66 | "apiVersion": "template.openshift.io/v1", 67 | "kind": "Template", 68 | "objects": objects, 69 | } 70 | 71 | if !withHardcodedNamespace { 72 | parameters := []map[string]interface{}{ 73 | { 74 | "name": "TAILOR_NAMESPACE", 75 | "required": true, 76 | }, 77 | } 78 | t["parameters"] = parameters 79 | } 80 | 81 | b, err := yaml.Marshal(t) 82 | if err != nil { 83 | return "", fmt.Errorf( 84 | "Could not marshal template: %s", err, 85 | ) 86 | } 87 | 88 | return string(b), err 89 | } 90 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/deploymentconfig/0/template.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Template 3 | parameters: 4 | - name: TAILOR_NAMESPACE 5 | required: true 6 | objects: 7 | - apiVersion: v1 8 | kind: Secret 9 | metadata: 10 | name: foo-user 11 | type: kubernetes.io/basic-auth 12 | data: 13 | password: c2VjcmV0 14 | username: dXNlcg== 15 | - apiVersion: apps.openshift.io/v1 16 | kind: DeploymentConfig 17 | metadata: 18 | labels: 19 | app: foo 20 | name: foo 21 | spec: 22 | replicas: 1 23 | revisionHistoryLimit: 10 24 | selector: 25 | app: foo 26 | strategy: 27 | activeDeadlineSeconds: 21600 28 | resources: {} 29 | rollingParams: 30 | intervalSeconds: 1 31 | maxSurge: 25% 32 | maxUnavailable: 25% 33 | timeoutSeconds: 600 34 | updatePeriodSeconds: 1 35 | type: Rolling 36 | template: 37 | metadata: 38 | labels: 39 | app: foo 40 | spec: 41 | containers: 42 | - env: 43 | - name: FOO 44 | value: abc 45 | - name: QUX 46 | valueFrom: 47 | secretKeyRef: 48 | key: username 49 | name: foo-user 50 | - name: BAZ 51 | value: http://baz.${TAILOR_NAMESPACE}.svc:8080/ 52 | image: docker-registry.default.svc:5000/${TAILOR_NAMESPACE}/foo:latest 53 | imagePullPolicy: Always 54 | livenessProbe: 55 | failureThreshold: 3 56 | httpGet: 57 | path: "/health" 58 | port: 8080 59 | scheme: HTTP 60 | initialDelaySeconds: 6 61 | periodSeconds: 10 62 | successThreshold: 1 63 | timeoutSeconds: 3 64 | name: foo 65 | ports: 66 | - containerPort: 8080 67 | protocol: TCP 68 | readinessProbe: 69 | failureThreshold: 3 70 | httpGet: 71 | path: "/health" 72 | port: 8080 73 | scheme: HTTP 74 | initialDelaySeconds: 3 75 | periodSeconds: 10 76 | successThreshold: 1 77 | timeoutSeconds: 3 78 | resources: 79 | limits: 80 | cpu: 100m 81 | memory: 128Mi 82 | requests: 83 | cpu: 50m 84 | memory: 128Mi 85 | terminationMessagePath: /dev/termination-log 86 | terminationMessagePolicy: File 87 | dnsPolicy: ClusterFirst 88 | restartPolicy: Always 89 | schedulerName: default-scheduler 90 | securityContext: {} 91 | terminationGracePeriodSeconds: 30 92 | test: false 93 | triggers: 94 | - type: ConfigChange 95 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL = /bin/bash 2 | .SHELLFLAGS := -eu -o pipefail -c 3 | .DELETE_ON_ERROR: 4 | MAKEFLAGS += --warn-undefined-variables 5 | MAKEFLAGS += --no-builtin-rules 6 | 7 | ## Run unit tests. 8 | test-unit: imports 9 | @(go list ./... | grep -v "vendor/" | grep -v "e2e" | xargs -n1 go test -cover) 10 | .PHONY: test-unit 11 | 12 | ## Run E2E tests (real binary against temporal, unique project in real cluster). 13 | test-e2e: imports internal/test/e2e/tailor-test 14 | @(go test -v -cover -timeout 20m github.com/opendevstack/tailor/internal/test/e2e) 15 | .PHONY: test-e2e 16 | 17 | ## Run all tests. 18 | test: test-unit test-e2e 19 | .PHONY: test 20 | 21 | ## Run goimports. 22 | imports: 23 | @(goimports -w .) 24 | .PHONY: imports 25 | 26 | ## Run gofmt. 27 | fmt: 28 | @(gofmt -w .) 29 | .PHONY: fmt 30 | 31 | ## Run golangci-lint. 32 | lint: 33 | @(go mod download && golangci-lint run) 34 | .PHONY: lint 35 | 36 | ## Install binary on current platform. 37 | install: imports 38 | @(cd cmd/tailor && go install -gcflags "all=-trimpath=$(CURDIR);$(shell go env GOPATH)") 39 | .PHONY: install 40 | 41 | ## Build binaries for all supported platforms. 42 | build: imports build-linux build-darwin build-windows 43 | .PHONY: build 44 | 45 | ## Build Linux binary. 46 | build-linux: imports 47 | cd cmd/tailor && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -gcflags "all=-trimpath=$(CURDIR);$(shell go env GOPATH)" -o tailor-linux-amd64 48 | .PHONY: build-linux 49 | 50 | ## Build macOS binary. 51 | build-darwin: imports 52 | cd cmd/tailor && GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -gcflags "all=-trimpath=$(CURDIR);$(shell go env GOPATH)" -o tailor-darwin-amd64 53 | .PHONY: build-darwin 54 | 55 | ## Build Windows binary. 56 | build-windows: imports 57 | cd cmd/tailor && GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -gcflags "all=-trimpath=$(CURDIR);$(shell go env GOPATH)" -o tailor-windows-amd64.exe 58 | .PHONY: build-windows 59 | 60 | internal/test/e2e/tailor-test: cmd/tailor/main.go go.mod go.sum pkg/cli/* pkg/commands/* pkg/openshift/* pkg/utils/* 61 | @(echo "Generating E2E test binary ...") 62 | @(cd cmd/tailor && go build -gcflags "all=-trimpath=$(CURDIR);$(shell go env GOPATH)" -o ../../internal/test/e2e/tailor-test) 63 | 64 | ### HELP 65 | ### Based on https://gist.github.com/prwhite/8168133#gistcomment-2278355. 66 | help: 67 | @echo '' 68 | @echo 'Usage:' 69 | @echo ' make ' 70 | @echo '' 71 | @echo 'Targets:' 72 | @awk '/^[a-zA-Z\-\_0-9]+:|^# .*/ { \ 73 | helpMessage = match(lastLine, /^## (.*)/); \ 74 | if (helpMessage) { \ 75 | helpCommand = substr($$1, 0, index($$1, ":")-1); \ 76 | helpMessage = substr(lastLine, RSTART + 3, RLENGTH); \ 77 | printf " %-35s %s\n", helpCommand, helpMessage; \ 78 | } else { \ 79 | printf "\n"; \ 80 | } \ 81 | } \ 82 | { lastLine = $$0 }' $(MAKEFILE_LIST) 83 | .PHONY: help 84 | -------------------------------------------------------------------------------- /pkg/commands/apply_test.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/opendevstack/tailor/internal/test/helper" 8 | "github.com/opendevstack/tailor/pkg/cli" 9 | "github.com/opendevstack/tailor/pkg/utils" 10 | ) 11 | 12 | type mockOcApplyClient struct { 13 | t *testing.T 14 | currentFixture string 15 | desiredFixture string 16 | } 17 | 18 | func (c *mockOcApplyClient) Export(target string, label string) ([]byte, error) { 19 | return helper.ReadFixtureFile(c.t, "command-apply/"+c.currentFixture), nil 20 | } 21 | 22 | func (c *mockOcApplyClient) Process(args []string) ([]byte, []byte, error) { 23 | return helper.ReadFixtureFile(c.t, "command-apply/"+c.desiredFixture), []byte(""), nil 24 | } 25 | 26 | func (c *mockOcApplyClient) Apply(config string, selector string) ([]byte, error) { 27 | return []byte(""), nil 28 | } 29 | 30 | func (c *mockOcApplyClient) Delete(kind string, name string) ([]byte, error) { 31 | return []byte(""), nil 32 | } 33 | 34 | func TestApply(t *testing.T) { 35 | tests := map[string]struct { 36 | namespace string 37 | nonInteractive bool 38 | stdinInput string 39 | currentFixture string 40 | desiredFixture string 41 | expectedDrift bool 42 | }{ 43 | "non-interactively": { 44 | namespace: "foo", 45 | nonInteractive: true, 46 | stdinInput: "", 47 | currentFixture: "current-list.yml", 48 | desiredFixture: "template-dir/desired-list.yml", 49 | expectedDrift: false, 50 | }, 51 | "interactively": { 52 | namespace: "foo", 53 | nonInteractive: false, 54 | stdinInput: "y\n", 55 | currentFixture: "current-list.yml", 56 | desiredFixture: "template-dir/desired-list.yml", 57 | expectedDrift: false, 58 | }, 59 | "interactively with select": { 60 | namespace: "foo", 61 | nonInteractive: false, 62 | stdinInput: "s\ny\nn\n", 63 | currentFixture: "current-list.yml", 64 | desiredFixture: "template-dir/desired-list.yml", 65 | expectedDrift: true, 66 | }, 67 | } 68 | for name, tc := range tests { 69 | t.Run(name, func(t *testing.T) { 70 | globalOptions := cli.InitGlobalOptions(&utils.OsFS{}) 71 | compareOptions := &cli.CompareOptions{ 72 | GlobalOptions: globalOptions, 73 | NamespaceOptions: &cli.NamespaceOptions{Namespace: tc.namespace}, 74 | TemplateDir: "../../internal/test/fixtures/command-apply/template-dir", 75 | ParamFiles: []string{}, 76 | } 77 | ocClient := &mockOcApplyClient{ 78 | currentFixture: tc.currentFixture, 79 | desiredFixture: tc.desiredFixture, 80 | } 81 | var stdin bytes.Buffer 82 | stdin.Write([]byte(tc.stdinInput)) 83 | drift, err := Apply(tc.nonInteractive, compareOptions, ocClient, &stdin) 84 | if err != nil { 85 | t.Fatal(err) 86 | } 87 | if drift != tc.expectedDrift { 88 | t.Fatalf("Want drift=%t, got drift=%t\n", tc.expectedDrift, drift) 89 | } 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /internal/test/e2e/testdata/deploymentconfig/0/want.out: -------------------------------------------------------------------------------- 1 | Comparing templates in deploymentconfig/0 with OCP namespace {{ .Project }}. 2 | Limiting resources to dc,secret. 3 | Found 0 resources in OCP cluster (current state) and 2 resources in processed templates (desired state). 4 | 5 | + secret/foo-user to create 6 | Secret drift is hidden. Use --reveal-secrets to see details. 7 | + dc/foo to create 8 | --- Current State (OpenShift cluster) 9 | +++ Desired State (Processed template) 10 | @@ -1 +1,81 @@ 11 | +apiVersion: apps.openshift.io/v1 12 | +kind: DeploymentConfig 13 | +metadata: 14 | + labels: 15 | + app: foo 16 | + name: foo 17 | +spec: 18 | + replicas: 1 19 | + revisionHistoryLimit: 10 20 | + selector: 21 | + app: foo 22 | + strategy: 23 | + activeDeadlineSeconds: 21600 24 | + resources: {} 25 | + rollingParams: 26 | + intervalSeconds: 1 27 | + maxSurge: 25% 28 | + maxUnavailable: 25% 29 | + timeoutSeconds: 600 30 | + updatePeriodSeconds: 1 31 | + type: Rolling 32 | + template: 33 | + metadata: 34 | + labels: 35 | + app: foo 36 | + spec: 37 | + containers: 38 | + - env: 39 | + - name: FOO 40 | + value: abc 41 | + - name: QUX 42 | + valueFrom: 43 | + secretKeyRef: 44 | + key: username 45 | + name: foo-user 46 | + - name: BAZ 47 | + value: http://baz.{{ .Project }}.svc:8080/ 48 | + image: docker-registry.default.svc:5000/{{ .Project }}/foo:latest 49 | + imagePullPolicy: Always 50 | + livenessProbe: 51 | + failureThreshold: 3 52 | + httpGet: 53 | + path: /health 54 | + port: 8080 55 | + scheme: HTTP 56 | + initialDelaySeconds: 6 57 | + periodSeconds: 10 58 | + successThreshold: 1 59 | + timeoutSeconds: 3 60 | + name: foo 61 | + ports: 62 | + - containerPort: 8080 63 | + protocol: TCP 64 | + readinessProbe: 65 | + failureThreshold: 3 66 | + httpGet: 67 | + path: /health 68 | + port: 8080 69 | + scheme: HTTP 70 | + initialDelaySeconds: 3 71 | + periodSeconds: 10 72 | + successThreshold: 1 73 | + timeoutSeconds: 3 74 | + resources: 75 | + limits: 76 | + cpu: 100m 77 | + memory: 128Mi 78 | + requests: 79 | + cpu: 50m 80 | + memory: 128Mi 81 | + terminationMessagePath: /dev/termination-log 82 | + terminationMessagePolicy: File 83 | + dnsPolicy: ClusterFirst 84 | + restartPolicy: Always 85 | + schedulerName: default-scheduler 86 | + securityContext: {} 87 | + terminationGracePeriodSeconds: 30 88 | + test: false 89 | + triggers: 90 | + - type: ConfigChange 91 | 92 | 93 | Summary: 0 in sync, 2 to create, 0 to update, 0 to delete 94 | 95 | Creating secret/foo-user ... done 96 | Creating dc/foo ... done 97 | -------------------------------------------------------------------------------- /pkg/openshift/test-public.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | xsFNBFxZldUBEADBk0AeCi0ZgEZJ6qt0RumoaqJ/fnur5ObgVgBZqqSr3lmDg5+g 4 | ujQpbpe+y6UCs5xZjqsrW6ecldNrwO7QG6Fpv9/wM2in+JWEnc1I+Sv5LkYR/8uO 5 | 7TZxryHEbthhEVzifeQwAlLbmARpyybwA7pJ8wjcGqOAKhuCSIivQt/6FuirqQqA 6 | Y9rfpn2SOfsGIE/vZRPLr5+bzRpsNppjTm5FM07zELDLSih/KvSYPUb5nsaCpYgF 7 | q1p/T9Urk20aMKMstwOp7+NeCfbOvRJsOZOPGFjGdSbY2drDOrU91xJYUVUhymOn 8 | fZ2o51G16aBGNwL2Kwzy/E6VIutiXQF/V+JOKw2gVO0m9cbxA2p7sVrLrJYmltDj 9 | 9IQ/BmmcCsx3yao5qs9kjKJvTllZ8WMlte9Tf11cleE40siWDKnyTParIn0iaoMh 10 | LD7YHfKcnMaM0cMyVa8cMSOGlWDrq1TKPW+reUxg4zh+ovYVnNbW0pwUy1c3fRiR 11 | gEvFhGkqWmquxXNF0aXT+P7r0nu+t49i5pDBL6fcIb7gCt53M0rUafQEyk981LWW 12 | iNeWOKJkuu7xnLHjw2p/wHXN4vV/3Q3aAgbpLZ2SAo3vTqTbtWMlx08sIRwV4lq2 13 | j+OPO2jAWLs5WacPprotZKuVkl7kDVEq2P4uaR8/D92/mawqq8maeF21+QARAQAB 14 | zTJ0ZXN0IChHZW5lcmF0ZWQgYnkgdGFpbG9yKSA8dGVzdEBvcGVuZGV2c3RhY2su 15 | b3JnPsLBaAQTAQgAHAUCXFmV1QkQtyKPJKP8hZ0CGwMCGQECCwcCFQgAADs1EAC5 16 | A5zVF6zGwa96ebFeqCTS/g4gXkn1YI6t3Nfsh8vy1egy6wfYeL7rBY20XcjEc5AG 17 | kzvxZgFHNcPA2ekPZgkFOWFt/U2XZSK/M9GA9MT9zqPvFPQGvyhzfB3mAxuVU2lJ 18 | ldQ+efSHl4k2BoLp4eI3HIw9m49HEVxUU9xbeARHEr258g2PZWqW41dFm5NZtQOS 19 | T4Q4s2lMe3WCYDu7d80B8pCGViv/mD6/pB3/HTmhwibeWZIfRjY88AMGEjtQVan1 20 | id3pbOuEhSszxdAoDsMLHi8uuje/ZeD7QFFLwbl0TRGmHs+EjF0xmY9P6TVxEzHV 21 | Gym9uZpExnuR15UifsSHVrmEGESC248b/JVBI4E1YyJo5LvlNzkUI5+Alh3LQPmU 22 | ai60/OrwFr7KdN69KkAYhbGNfU5lnxSc9bsaAiVI7AtdT2iA6u/jU3IynoLwZzJF 23 | M4koGc0dfSJ6ZpNycyAu2GmkjYZx9nSlB6U1mPvjCJFAO8gTqNHhjyR/bLgidC/u 24 | kNzC2i322mfQqhCTbuWd7VFUdrz04+FO+EeeRNoQL9jQgTQFiWHS11sqIiSGArIV 25 | eXZPaxi3pEWZK+roBnUNldVbFpspPNRCS6J8S5bQL7YpiVtb8BSIOitDhwzgDYVr 26 | 9B6UD5rQdFNjK962y1uQWqPgGq/KywOfu7UtuGIRXc7BTQRcWZXVARAA2D7cBqfx 27 | QzeNfenCYjziSsJ9WXkJWdTZpgBsVlEL4+LOPN2p9C29oVUgY6FUy4mDUahL1cn5 28 | USMq1gJ1DMd20J0S4MI0SF3udYaxmOA3Vg3EJkdEE6BiQjD7XKaugF/ROnk0kDio 29 | tYHCuob9CNGq87R42e39jVVkXPbsaO+wx7ZGMGFPY6955VQgCyHaAHTfOzerKMHJ 30 | WXrPKVs6GFGbkUfTzMomg8ZGHTtY6mXjfjLYvmljgHD7Vtus2R2FBdzCgfKVmg3i 31 | +kyvjEoFEAWibQDg8HweeCWii9UQ9/6zDtlj+1A0yuRpDD5ZbfBdm951uPlyzhWO 32 | nU+5iroVZlKEIc2qKwsNeQSA3iYlZhlZDd6DFnh7Mi8Bn4w7c+2A5MAZbpLmHEcC 33 | rWIQ/kaJnY5S1QwE9I2CFwcFpF1I9yDJguCjjGRcwYYqul20kl73Af7X2YDvxjx8 34 | tkW5AzRxle6Bnx43VnlzuZmIKEqCzU6NIg4/gVu0AnaFY87rolWu7nLFIBEZgGJI 35 | y1Y46+bo2OEIBWGl/JBVxIklw9wRb+9a/ofNBuaKFd0EbZlM4ItG2K/DujJfJUy4 36 | TY4gFnDNU8veiouCyKOrtCuXTuYwbLMNS5scWBVIoMjXsgWQouPIRkxjEr44EpdX 37 | 27DWyxo/MFqdBF4fSE0vOZsu0+ZCkItobokAEQEAAcLBXwQYAQgAEwUCXFmV1QkQ 38 | tyKPJKP8hZ0CGwwAACJoEACLKv62H2rTBKwFfNaHWV2NagqF6j2AaPds/A4NNTFP 39 | jDneK8hDRDkuQjTDIPlMn9Y8k1s/hqUv54g87xC3i1d40AQt6Oumpvz9h3zmALol 40 | W/AMpi/wFLNdNh4WwKXNm2yydloeKozTKRNxF5alqsMPKoStM8lphYOa8jNXaebu 41 | 9Io/YwZcdJ9SwgUdCFeZu0866tgUXT2H7IFui/x+TPjw6eFX6NsOKozInwB+qumD 42 | fsY5dfnn8GTpbVhCI+xpRFpTuU1576mF0SR+VqQhzqstYHvBSBr9XFWXyF8Lmbtk 43 | Xo7+5P7jj3OjC+yYdUcuyhC2s0j7pLhKolPfs9y71hmGWQj4pfcrn4zMVSJEQcAj 44 | OLapvFf1ctgK0itYERmV4uM4JR+DLwtM1D8vOYe3R8/ijUWOAAeR7WLV7rWzJuON 45 | eacDeCrKF0OgCOsxBF3JYFDBvycc4DNRoszPTD5cCYhsN+Jc8OR7kGGIJlm6gdJ+ 46 | VflNjK7RUV3cfVHFlkDhWWP3GfydxRh3y/afagM7Az5VjJ3j1gWsmH0WKhb8QA/0 47 | kaDKxeF+NF9aLFOmE0oCV9ZV680VrugxigO5rgwCiCE4qKBW1Ue1RwHtNCJlE7jT 48 | m+msedrkaWTSdysQ0l+foOr0cdKoioFDaLBVW7N4s1uhT+QFTvMO/MhdCKN9oJx7 49 | Lw== 50 | =s6MK 51 | -----END PGP PUBLIC KEY BLOCK----- 52 | -------------------------------------------------------------------------------- /pkg/openshift/change.go: -------------------------------------------------------------------------------- 1 | package openshift 2 | 3 | import ( 4 | "github.com/pmezard/go-difflib/difflib" 5 | ) 6 | 7 | var ( 8 | kindToShortMapping = map[string]string{ 9 | "Service": "svc", 10 | "Route": "route", 11 | "DeploymentConfig": "dc", 12 | "Deployment": "deployment", 13 | "BuildConfig": "bc", 14 | "ImageStream": "is", 15 | "PersistentVolumeClaim": "pvc", 16 | "Template": "template", 17 | "ConfigMap": "cm", 18 | "Secret": "secret", 19 | "RoleBinding": "rolebinding", 20 | "ServiceAccount": "serviceaccount", 21 | "CronJob": "cronjob", 22 | "Job": "job", 23 | "LimitRange": "limitrange", 24 | "ResourceQuota": "quota", 25 | "HorizontalPodAutoscaler": "hpa", 26 | "StatefulSet": "statefulset", 27 | } 28 | ) 29 | 30 | // Change is a description of a drift between current and desired state, and 31 | // the required patches to bring them back in sync. 32 | type Change struct { 33 | Action string 34 | Kind string 35 | Name string 36 | CurrentState string 37 | DesiredState string 38 | } 39 | 40 | // NewChange creates a new change for given template/platform item. 41 | func NewChange(templateItem *ResourceItem, platformItem *ResourceItem) *Change { 42 | c := &Change{ 43 | Kind: templateItem.Kind, 44 | Name: templateItem.Name, 45 | CurrentState: platformItem.YamlConfig(), 46 | DesiredState: templateItem.YamlConfig(), 47 | } 48 | 49 | if platformItem.YamlConfig() != templateItem.YamlConfig() { 50 | c.Action = "Update" 51 | } else { 52 | c.Action = "Noop" 53 | } 54 | 55 | return c 56 | } 57 | 58 | // ItemName returns the kind/name of the resource the change relates to. 59 | func (c *Change) ItemName() string { 60 | return kindToShortMapping[c.Kind] + "/" + c.Name 61 | } 62 | 63 | // Diff returns a unified diff text for the change. 64 | func (c *Change) Diff(revealSecrets bool) string { 65 | if c.isSecret() && !revealSecrets { 66 | return "Secret drift is hidden. Use --reveal-secrets to see details.\n" 67 | } 68 | diff := difflib.UnifiedDiff{ 69 | A: difflib.SplitLines(c.CurrentState), 70 | B: difflib.SplitLines(c.DesiredState), 71 | FromFile: "Current State (OpenShift cluster)", 72 | ToFile: "Desired State (Processed template)", 73 | Context: 3, 74 | } 75 | text, _ := difflib.GetUnifiedDiffString(diff) 76 | return text 77 | } 78 | 79 | func (c *Change) isSecret() bool { 80 | return kindToShortMapping[c.Kind] == "secret" 81 | } 82 | 83 | func recreateChanges(templateItem, platformItem *ResourceItem) []*Change { 84 | deleteChange := &Change{ 85 | Action: "Delete", 86 | Kind: templateItem.Kind, 87 | Name: templateItem.Name, 88 | CurrentState: platformItem.YamlConfig(), 89 | DesiredState: "", 90 | } 91 | createChange := &Change{ 92 | Action: "Create", 93 | Kind: templateItem.Kind, 94 | Name: templateItem.Name, 95 | CurrentState: "", 96 | DesiredState: templateItem.YamlConfig(), 97 | } 98 | return []*Change{deleteChange, createChange} 99 | } 100 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI= 2 | github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= 3 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= 4 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 10 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 11 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 12 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 13 | github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 14 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 15 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 16 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 17 | github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= 18 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 19 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 22 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 23 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 24 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= 25 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 26 | golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac h1:7d7lG9fHOLdL6jZPtnV4LpI41SbohIJ1Atq7U991dMg= 27 | golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 28 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= 29 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 30 | golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0= 31 | golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 33 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 34 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 35 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 36 | -------------------------------------------------------------------------------- /pkg/openshift/export_test.go: -------------------------------------------------------------------------------- 1 | package openshift 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | "github.com/opendevstack/tailor/internal/test/helper" 8 | ) 9 | 10 | type mockOcExportClient struct { 11 | t *testing.T 12 | fixture string 13 | } 14 | 15 | func (c *mockOcExportClient) Export(target string, label string) ([]byte, error) { 16 | return helper.ReadFixtureFile(c.t, "export/"+c.fixture), nil 17 | } 18 | 19 | func newResourceFilterOrFatal(t *testing.T, kindArg string, selectorFlag string, excludes []string) *ResourceFilter { 20 | filter, err := NewResourceFilter(kindArg, selectorFlag, excludes) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | return filter 25 | } 26 | 27 | func TestExportAsTemplateFile(t *testing.T) { 28 | tests := map[string]struct { 29 | fixture string 30 | goldenTemplate string 31 | filter *ResourceFilter 32 | withAnnotations bool 33 | trimAnnotations []string 34 | namespace string 35 | withHardcodedNamespace bool 36 | }{ 37 | "Without all annotations": { 38 | fixture: "is.yml", 39 | goldenTemplate: "is.yml", 40 | filter: newResourceFilterOrFatal(t, "is", "", []string{}), 41 | withAnnotations: false, 42 | trimAnnotations: []string{}, 43 | namespace: "foo", 44 | withHardcodedNamespace: true, 45 | }, 46 | "With all annotations": { 47 | fixture: "is.yml", 48 | goldenTemplate: "is-annotation.yml", 49 | filter: newResourceFilterOrFatal(t, "is", "", []string{}), 50 | withAnnotations: true, 51 | trimAnnotations: []string{}, 52 | namespace: "foo", 53 | withHardcodedNamespace: true, 54 | }, 55 | "With trimmed annotation": { 56 | fixture: "is.yml", 57 | goldenTemplate: "is-trimmed-annotation.yml", 58 | filter: newResourceFilterOrFatal(t, "is", "", []string{}), 59 | withAnnotations: false, 60 | trimAnnotations: []string{"description"}, 61 | namespace: "foo", 62 | withHardcodedNamespace: true, 63 | }, 64 | "With trimmed annotation prefix": { 65 | fixture: "is.yml", 66 | goldenTemplate: "is-trimmed-annotation-prefix.yml", 67 | filter: newResourceFilterOrFatal(t, "is", "", []string{}), 68 | withAnnotations: false, 69 | trimAnnotations: []string{"openshift.io/"}, 70 | namespace: "foo", 71 | withHardcodedNamespace: true, 72 | }, 73 | "With TAILOR_NAMESPACE": { 74 | fixture: "bc.yml", 75 | goldenTemplate: "bc.yml", 76 | filter: newResourceFilterOrFatal(t, "bc", "", []string{}), 77 | withAnnotations: false, 78 | trimAnnotations: []string{}, 79 | namespace: "foo-dev", 80 | withHardcodedNamespace: false, 81 | }, 82 | "Works with generateName": { 83 | fixture: "rolebinding-generate-name.yml", 84 | goldenTemplate: "rolebinding-generate-name.yml", 85 | filter: newResourceFilterOrFatal(t, "rolebinding", "", []string{}), 86 | withAnnotations: false, 87 | trimAnnotations: []string{}, 88 | namespace: "foo", 89 | withHardcodedNamespace: true, 90 | }, 91 | "Respects filter": { 92 | fixture: "is.yml", 93 | goldenTemplate: "empty.yml", 94 | filter: newResourceFilterOrFatal(t, "bc", "", []string{}), 95 | withAnnotations: false, 96 | namespace: "foo", 97 | withHardcodedNamespace: true, 98 | }, 99 | } 100 | 101 | for name, tc := range tests { 102 | t.Run(name, func(t *testing.T) { 103 | c := &mockOcExportClient{t: t, fixture: tc.fixture} 104 | actual, err := ExportAsTemplateFile(tc.filter, tc.withAnnotations, tc.namespace, tc.withHardcodedNamespace, tc.trimAnnotations, c) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | 109 | expected := string(helper.ReadGoldenFile(t, "export/"+tc.goldenTemplate)) 110 | 111 | if diff := cmp.Diff(expected, actual); diff != "" { 112 | t.Fatalf("Expected template mismatch (-want +got):\n%s", diff) 113 | } 114 | }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /pkg/utils/encryption.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "encoding/base64" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | 12 | "golang.org/x/crypto/openpgp" 13 | "golang.org/x/crypto/openpgp/armor" 14 | "golang.org/x/crypto/openpgp/packet" 15 | ) 16 | 17 | func CreateEntity(name, email string) (*openpgp.Entity, error) { 18 | var e *openpgp.Entity 19 | conf := &packet.Config{ 20 | RSABits: 4096, 21 | DefaultHash: crypto.SHA256, 22 | DefaultCipher: packet.CipherFunction(packet.CipherAES128), 23 | } 24 | e, err := openpgp.NewEntity(name, "Generated by tailor", email, conf) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | // Sign all the identities 30 | for _, id := range e.Identities { 31 | err := id.SelfSignature.SignUserId(id.UserId.Id, e.PrimaryKey, e.PrivateKey, nil) 32 | if err != nil { 33 | return nil, err 34 | } 35 | } 36 | return e, nil 37 | } 38 | 39 | func PrintPublicKey(entity *openpgp.Entity, filename string) error { 40 | f, err := os.Create(filename) 41 | if err != nil { 42 | return err 43 | } 44 | w, err := armor.Encode(f, openpgp.PublicKeyType, nil) 45 | if err != nil { 46 | return err 47 | } 48 | defer w.Close() 49 | err = entity.Serialize(w) 50 | if err != nil { 51 | return err 52 | } 53 | _, err = io.WriteString(f, "\n") 54 | if err != nil { 55 | return err 56 | } 57 | return nil 58 | } 59 | 60 | func PrintPrivateKey(entity *openpgp.Entity, filename string) error { 61 | f, err := os.Create(filename) 62 | if err != nil { 63 | return err 64 | } 65 | w, err := armor.Encode(f, openpgp.PrivateKeyType, nil) 66 | if err != nil { 67 | return err 68 | } 69 | err = entity.SerializePrivate(w, nil) 70 | if err != nil { 71 | return err 72 | } 73 | w.Close() 74 | _, err = io.WriteString(f, "\n") 75 | if err != nil { 76 | return err 77 | } 78 | return nil 79 | } 80 | 81 | // Assembles entity list from keys in given files 82 | func GetEntityList(keys []string, passphrase string) (openpgp.EntityList, error) { 83 | entityList := openpgp.EntityList{} 84 | for _, filename := range keys { 85 | keyringFileBuffer, _ := os.Open(filename) 86 | defer keyringFileBuffer.Close() 87 | l, err := openpgp.ReadArmoredKeyRing(keyringFileBuffer) 88 | if err != nil { 89 | return entityList, fmt.Errorf( 90 | "Reading key '%s' failed: %s", 91 | filename, 92 | err, 93 | ) 94 | } 95 | entity := l[0] 96 | 97 | // Decrypt private key using passphrase 98 | passphraseBytes := []byte(passphrase) 99 | if entity.PrivateKey != nil && entity.PrivateKey.Encrypted { 100 | err := entity.PrivateKey.Decrypt(passphraseBytes) 101 | if err != nil { 102 | return entityList, fmt.Errorf("Failed to decrypt key: %s", err) 103 | } 104 | } 105 | for _, subkey := range entity.Subkeys { 106 | if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted { 107 | err := subkey.PrivateKey.Decrypt(passphraseBytes) 108 | if err != nil { 109 | return entityList, fmt.Errorf( 110 | "Failed to decrypt subkey: %s", err, 111 | ) 112 | } 113 | } 114 | } 115 | 116 | entityList = append(entityList, entity) 117 | } 118 | return entityList, nil 119 | } 120 | 121 | // Encrypts secret with all public keys and base64-encodes the result. 122 | func Encrypt(secret string, entityList openpgp.EntityList) (string, error) { 123 | // Encrypt message using public keys 124 | buf := new(bytes.Buffer) 125 | w, err := openpgp.Encrypt(buf, entityList, nil, nil, nil) 126 | if err != nil { 127 | return "", fmt.Errorf("Encrypting '%s' failed: %s", secret, err) 128 | } 129 | _, err = w.Write([]byte(secret)) 130 | if err != nil { 131 | return "", err 132 | } 133 | err = w.Close() 134 | if err != nil { 135 | return "", err 136 | } 137 | 138 | // Return as base64 encoded string 139 | bytes, err := ioutil.ReadAll(buf) 140 | if err != nil { 141 | return "", err 142 | } 143 | str := base64.StdEncoding.EncodeToString(bytes) 144 | return str, nil 145 | } 146 | 147 | // Decrypts the base64-encoded string end decrypts with the private key. 148 | func Decrypt(encoded string, entityList openpgp.EntityList) (string, error) { 149 | // Decode bas64-encoded string 150 | encrypted, err := base64.StdEncoding.DecodeString(encoded) 151 | if err != nil { 152 | return "", fmt.Errorf("Decoding '%s' failed: %s", encoded, err) 153 | } 154 | 155 | // Decrypt encrypted message 156 | buf := bytes.NewBuffer([]byte(encrypted)) 157 | md, err := openpgp.ReadMessage(buf, entityList, nil, nil) 158 | if err != nil { 159 | return "", fmt.Errorf("Decrypting '%s' failed: %s", encoded, err) 160 | } 161 | bytes, err := ioutil.ReadAll(md.UnverifiedBody) 162 | return string(bytes), err 163 | } 164 | -------------------------------------------------------------------------------- /pkg/openshift/list_test.go: -------------------------------------------------------------------------------- 1 | package openshift 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestConfigFilterByKind(t *testing.T) { 8 | byteList := []byte( 9 | `apiVersion: v1 10 | items: 11 | - apiVersion: v1 12 | kind: PersistentVolumeClaim 13 | metadata: 14 | name: foo 15 | spec: 16 | accessModes: 17 | - ReadWriteOnce 18 | resources: 19 | requests: 20 | storage: 5Gi 21 | storageClassName: gp2 22 | status: {} 23 | - apiVersion: v1 24 | kind: ConfigMap 25 | metadata: 26 | name: bar 27 | data: 28 | bar: baz 29 | kind: List 30 | metadata: {} 31 | `) 32 | 33 | filter := &ResourceFilter{ 34 | Kinds: []string{"PersistentVolumeClaim"}, 35 | Name: "", 36 | Label: "", 37 | } 38 | 39 | list, _ := NewTemplateBasedResourceList(filter, byteList) 40 | 41 | if len(list.Items) != 1 { 42 | t.Errorf("One item should have been extracted, got %v items.", len(list.Items)) 43 | return 44 | } 45 | 46 | item := list.Items[0] 47 | if item.Kind != "PersistentVolumeClaim" { 48 | t.Errorf("Item should have been a PersistentVolumeClaim, got %s.", item.Kind) 49 | } 50 | } 51 | 52 | func TestConfigFilterByName(t *testing.T) { 53 | byteList := []byte( 54 | `apiVersion: v1 55 | items: 56 | - apiVersion: v1 57 | kind: PersistentVolumeClaim 58 | metadata: 59 | name: foo 60 | spec: 61 | accessModes: 62 | - ReadWriteOnce 63 | resources: 64 | requests: 65 | storage: 5Gi 66 | storageClassName: gp2 67 | status: {} 68 | - apiVersion: v1 69 | kind: PersistentVolumeClaim 70 | metadata: 71 | name: bar 72 | spec: 73 | accessModes: 74 | - ReadWriteOnce 75 | resources: 76 | requests: 77 | storage: 1Gi 78 | storageClassName: gp2 79 | status: {} 80 | kind: List 81 | metadata: {} 82 | `) 83 | 84 | filter := &ResourceFilter{ 85 | Kinds: []string{}, 86 | Name: "PersistentVolumeClaim/foo", 87 | Label: "", 88 | } 89 | 90 | list, _ := NewTemplateBasedResourceList(filter, byteList) 91 | 92 | if len(list.Items) != 1 { 93 | t.Errorf("One item should have been extracted, got %v items.", len(list.Items)) 94 | return 95 | } 96 | 97 | item := list.Items[0] 98 | if item.Name != "foo" { 99 | t.Errorf("Item should have had name foo, got %s.", item.Name) 100 | } 101 | } 102 | 103 | func TestConfigFilterBySelector(t *testing.T) { 104 | byteList := []byte( 105 | `apiVersion: v1 106 | items: 107 | - apiVersion: v1 108 | kind: PersistentVolumeClaim 109 | metadata: 110 | labels: 111 | app: foo 112 | name: foo 113 | spec: 114 | accessModes: 115 | - ReadWriteOnce 116 | resources: 117 | requests: 118 | storage: 5Gi 119 | storageClassName: gp2 120 | status: {} 121 | - apiVersion: v1 122 | kind: PersistentVolumeClaim 123 | metadata: 124 | labels: 125 | app: bar 126 | name: bar 127 | spec: 128 | accessModes: 129 | - ReadWriteOnce 130 | resources: 131 | requests: 132 | storage: 1Gi 133 | storageClassName: gp2 134 | status: {} 135 | - apiVersion: v1 136 | kind: ConfigMap 137 | metadata: 138 | labels: 139 | app: foo 140 | name: foo 141 | data: 142 | bar: baz 143 | - apiVersion: v1 144 | kind: ConfigMap 145 | metadata: 146 | labels: 147 | app: bar 148 | name: bar 149 | data: 150 | bar: baz 151 | - apiVersion: v1 152 | data: 153 | auth-token: abcdef 154 | kind: Secret 155 | metadata: 156 | name: bar 157 | labels: 158 | app: bar 159 | type: opaque 160 | kind: List 161 | metadata: {} 162 | `) 163 | 164 | pvcFilter := &ResourceFilter{ 165 | Kinds: []string{"PersistentVolumeClaim"}, 166 | Name: "", 167 | Label: "app=foo", 168 | } 169 | cmFilter := &ResourceFilter{ 170 | Kinds: []string{"ConfigMap"}, 171 | Name: "", 172 | Label: "app=foo", 173 | } 174 | secretFilter := &ResourceFilter{ 175 | Kinds: []string{"Secret"}, 176 | Name: "", 177 | Label: "app=foo", 178 | } 179 | 180 | pvcList, _ := NewTemplateBasedResourceList(pvcFilter, byteList) 181 | 182 | if len(pvcList.Items) != 1 { 183 | t.Errorf("One item should have been extracted, got %v items.", len(pvcList.Items)) 184 | } 185 | 186 | _, err := pvcList.getItem("PersistentVolumeClaim", "foo") 187 | if err != nil { 188 | t.Errorf("Item foo should have been present.") 189 | } 190 | 191 | cmList, _ := NewTemplateBasedResourceList(cmFilter, byteList) 192 | 193 | if len(cmList.Items) != 1 { 194 | t.Errorf("One item should have been extracted, got %v items.", len(cmList.Items)) 195 | } 196 | 197 | _, err = cmList.getItem("ConfigMap", "foo") 198 | if err != nil { 199 | t.Errorf("Item should have been present.") 200 | } 201 | 202 | secretList, _ := NewTemplateBasedResourceList(secretFilter, byteList) 203 | 204 | if len(secretList.Items) != 0 { 205 | t.Errorf("No item should have been extracted, got %v items.", len(secretList.Items)) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /pkg/cli/options_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | "github.com/opendevstack/tailor/internal/test/helper" 8 | "github.com/opendevstack/tailor/pkg/utils" 9 | ) 10 | 11 | func TestResolvedFile(t *testing.T) { 12 | tests := map[string]struct { 13 | fileFlag string 14 | namespaceFlag string 15 | fs utils.FileStater 16 | expected string 17 | }{ 18 | "no file flag and no namespace flag given": { 19 | fileFlag: "Tailorfile", // default 20 | namespaceFlag: "", 21 | fs: &helper.SomeFilesExistFS{Existing: []string{"Tailorfile"}}, 22 | expected: "Tailorfile", 23 | }, 24 | "no file flag given but namespace flag given and namespaced file exists": { 25 | fileFlag: "Tailorfile", // default 26 | namespaceFlag: "foo", 27 | fs: &helper.SomeFilesExistFS{Existing: []string{"Tailorfile.foo"}}, 28 | expected: "Tailorfile.foo", 29 | }, 30 | "no file flag given but namespace flag given and namespaced file does not exist": { 31 | fileFlag: "Tailorfile", // default 32 | namespaceFlag: "foo", 33 | fs: &helper.SomeFilesExistFS{}, 34 | expected: "Tailorfile", 35 | }, 36 | "file flag given and no namespace flag given": { 37 | fileFlag: "mytailorfile", 38 | namespaceFlag: "", 39 | fs: &helper.SomeFilesExistFS{Existing: []string{"mytailorfile"}}, 40 | expected: "mytailorfile", 41 | }, 42 | "file flag and namespace flag given": { 43 | fileFlag: "mytailorfile", 44 | namespaceFlag: "foo", 45 | fs: &helper.SomeFilesExistFS{Existing: []string{"mytailorfile"}}, 46 | expected: "mytailorfile", 47 | }, 48 | } 49 | for name, tc := range tests { 50 | t.Run(name, func(t *testing.T) { 51 | o := InitGlobalOptions(tc.fs) 52 | o.File = tc.fileFlag 53 | actual := o.resolvedFile(tc.namespaceFlag) 54 | if actual != tc.expected { 55 | t.Fatalf("Expected file: '%s', got: '%s'", tc.expected, actual) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestNewCompareOptionsExcludes(t *testing.T) { 62 | tests := map[string]struct { 63 | excludeFlag []string 64 | wantExcludes []string 65 | }{ 66 | "none": { 67 | excludeFlag: []string{}, 68 | wantExcludes: []string{}, 69 | }, 70 | "passed once": { 71 | excludeFlag: []string{"bc"}, 72 | wantExcludes: []string{"bc"}, 73 | }, 74 | "passed once comma-separated": { 75 | excludeFlag: []string{"bc,is"}, 76 | wantExcludes: []string{"bc", "is"}, 77 | }, 78 | "passed multiple times": { 79 | excludeFlag: []string{"bc", "is"}, 80 | wantExcludes: []string{"bc", "is"}, 81 | }, 82 | "passed multiple times and comma-separated": { 83 | excludeFlag: []string{"bc,is", "route"}, 84 | wantExcludes: []string{"bc", "is", "route"}, 85 | }, 86 | } 87 | for name, tc := range tests { 88 | t.Run(name, func(t *testing.T) { 89 | o, err := NewGlobalOptions(false, "Tailorfile", false, false, false, "oc", false) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | got, err := NewCompareOptions( 94 | o, 95 | "", 96 | "", 97 | tc.excludeFlag, 98 | ".", 99 | ".", 100 | "", 101 | "", 102 | "", 103 | "", 104 | []string{}, 105 | []string{}, 106 | []string{}, 107 | false, 108 | false, 109 | false, 110 | false, 111 | false, 112 | false, 113 | "") 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | if diff := cmp.Diff(tc.wantExcludes, got.Excludes); diff != "" { 118 | t.Errorf("Compare options mismatch (-want +got):\n%s", diff) 119 | } 120 | }) 121 | } 122 | } 123 | 124 | func TestNewExportOptionsExcludes(t *testing.T) { 125 | tests := map[string]struct { 126 | excludeFlag []string 127 | wantExcludes []string 128 | }{ 129 | "none": { 130 | excludeFlag: []string{}, 131 | wantExcludes: []string{}, 132 | }, 133 | "passed once": { 134 | excludeFlag: []string{"bc"}, 135 | wantExcludes: []string{"bc"}, 136 | }, 137 | "passed once comma-separated": { 138 | excludeFlag: []string{"bc,is"}, 139 | wantExcludes: []string{"bc", "is"}, 140 | }, 141 | "passed multiple times": { 142 | excludeFlag: []string{"bc", "is"}, 143 | wantExcludes: []string{"bc", "is"}, 144 | }, 145 | "passed multiple times and comma-separated": { 146 | excludeFlag: []string{"bc,is", "route"}, 147 | wantExcludes: []string{"bc", "is", "route"}, 148 | }, 149 | } 150 | for name, tc := range tests { 151 | t.Run(name, func(t *testing.T) { 152 | o, err := NewGlobalOptions(false, "Tailorfile", false, false, false, "oc", false) 153 | if err != nil { 154 | t.Fatal(err) 155 | } 156 | got, err := NewExportOptions( 157 | o, 158 | "", 159 | "", 160 | tc.excludeFlag, 161 | ".", 162 | ".", 163 | false, 164 | false, 165 | []string{}, 166 | "") 167 | if err != nil { 168 | t.Fatal(err) 169 | } 170 | if diff := cmp.Diff(tc.wantExcludes, got.Excludes); diff != "" { 171 | t.Errorf("Export options mismatch (-want +got):\n%s", diff) 172 | } 173 | }) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /pkg/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "strings" 13 | 14 | "github.com/fatih/color" 15 | ) 16 | 17 | var verbose bool 18 | var debug bool 19 | var ocBinary string 20 | 21 | // PrintGreenf prints in green. 22 | var PrintGreenf func(format string, a ...interface{}) 23 | 24 | // FprintGreenf prints in green to w. 25 | var FprintGreenf func(w io.Writer, format string, a ...interface{}) 26 | 27 | // PrintBluef prints in blue. 28 | var PrintBluef func(format string, a ...interface{}) 29 | 30 | // FprintBluef prints in green to w. 31 | var FprintBluef func(w io.Writer, format string, a ...interface{}) 32 | 33 | // PrintYellowf prints in yellow. 34 | var PrintYellowf func(format string, a ...interface{}) 35 | 36 | // FprintYellowf prints in green to w. 37 | var FprintYellowf func(w io.Writer, format string, a ...interface{}) 38 | 39 | // PrintRedf prints in red. 40 | var PrintRedf func(format string, a ...interface{}) 41 | 42 | // FprintRedf prints in green to w. 43 | var FprintRedf func(w io.Writer, format string, a ...interface{}) 44 | 45 | func init() { 46 | color.Output = os.Stderr 47 | PrintGreenf = color.New(color.FgGreen).PrintfFunc() 48 | PrintBluef = color.New(color.FgBlue).PrintfFunc() 49 | PrintYellowf = color.New(color.FgYellow).PrintfFunc() 50 | PrintRedf = color.New(color.FgRed).PrintfFunc() 51 | FprintGreenf = color.New(color.FgGreen).FprintfFunc() 52 | FprintBluef = color.New(color.FgBlue).FprintfFunc() 53 | FprintYellowf = color.New(color.FgYellow).FprintfFunc() 54 | FprintRedf = color.New(color.FgRed).FprintfFunc() 55 | verbose = false 56 | } 57 | 58 | // VerboseMsg prints given message when verbose mode is on. 59 | // Verbose mode is implicitly turned on when debug mode is on. 60 | func VerboseMsg(messages ...string) { 61 | if verbose { 62 | PrintBluef("--> %s\n", strings.Join(messages, " ")) 63 | } 64 | } 65 | 66 | // DebugMsg prints given message when debug mode is on. 67 | func DebugMsg(messages ...string) { 68 | if debug { 69 | PrintBluef("--> %s\n", strings.Join(messages, " ")) 70 | } 71 | } 72 | 73 | // ExecOcCmd executes "oc" with given namespace and selector applied. 74 | func ExecOcCmd(args []string, namespace string, selector string) *exec.Cmd { 75 | if len(namespace) > 0 { 76 | args = append(args, "--namespace="+namespace) 77 | } 78 | if len(selector) > 0 { 79 | args = append(args, "--selector="+selector) 80 | } 81 | return ExecPlainOcCmd(args) 82 | } 83 | 84 | // ExecPlainOcCmd executes "oc" with given arguments applied. 85 | func ExecPlainOcCmd(args []string) *exec.Cmd { 86 | return execCmd(ocBinary, args) 87 | } 88 | 89 | // RunCmd runs the given command and returns the result 90 | func RunCmd(cmd *exec.Cmd) (outBytes, errBytes []byte, err error) { 91 | var stdout, stderr bytes.Buffer 92 | cmd.Stdout = &stdout 93 | cmd.Stderr = &stderr 94 | err = cmd.Run() 95 | outBytes = stdout.Bytes() 96 | errBytes = stderr.Bytes() 97 | return outBytes, errBytes, err 98 | } 99 | 100 | func execCmd(executable string, args []string) *exec.Cmd { 101 | VerboseMsg(executable + " " + strings.Join(args, " ")) 102 | return exec.Command(executable, args...) 103 | } 104 | 105 | // AskForAction asks the user the given question. A user must type in one of the presented options and 106 | // then press enter.If the input is not recognized, it will ask again. The function does not return 107 | // until it gets a valid response from the user. 108 | // Options are of form "y=yes". The matching is fuzzy, which means allowed values are 109 | // "y", "Y", "yes", "YES", "Yes" and so on. The returned value is always the "key" ("y" in this case), 110 | // regardless if the input was "y" or "yes" etc. 111 | func AskForAction(question string, options []string, reader *bufio.Reader) string { 112 | validAnswers := map[string]string{} 113 | for _, v := range options { 114 | p := strings.Split(v, "=") 115 | validAnswers[p[0]] = p[0] 116 | validAnswers[p[1]] = p[0] 117 | } 118 | 119 | for { 120 | fmt.Printf("%s [%s]: ", question, strings.Join(options, ", ")) 121 | 122 | answer, err := reader.ReadString('\n') 123 | if err != nil { 124 | log.Fatal(err) 125 | } 126 | 127 | answer = strings.ToLower(strings.TrimSpace(answer)) 128 | 129 | if v, ok := validAnswers[answer]; !ok { 130 | fmt.Printf("'%s' is not a valid option. Please try again.\n", answer) 131 | } else { 132 | return v 133 | } 134 | } 135 | } 136 | 137 | // EditEnvFile opens content in EDITOR, and returns saved content. 138 | func EditEnvFile(content string) (string, error) { 139 | err := ioutil.WriteFile(".ENV.DEC", []byte(content), 0644) 140 | if err != nil { 141 | return "", err 142 | } 143 | editor := os.Getenv("EDITOR") 144 | if len(editor) == 0 { 145 | editor = "vim" 146 | } 147 | 148 | _, err = exec.LookPath(editor) 149 | if err != nil { 150 | return "", fmt.Errorf( 151 | "Please install '%s' or set/change $EDITOR", 152 | editor, 153 | ) 154 | } 155 | 156 | cmd := exec.Command(editor, ".ENV.DEC") 157 | cmd.Stdin = os.Stdin 158 | cmd.Stdout = os.Stdout 159 | err = cmd.Run() 160 | if err != nil { 161 | return "", err 162 | } 163 | data, err := ioutil.ReadFile(".ENV.DEC") 164 | if err != nil { 165 | return "", err 166 | } 167 | os.Remove(".ENV.DEC") 168 | return string(data), nil 169 | } 170 | -------------------------------------------------------------------------------- /pkg/openshift/filter.go: -------------------------------------------------------------------------------- 1 | package openshift 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/opendevstack/tailor/pkg/utils" 10 | ) 11 | 12 | var availableKinds = []string{ 13 | "Service", 14 | "Route", 15 | "DeploymentConfig", 16 | "Deployment", 17 | "BuildConfig", 18 | "ImageStream", 19 | "PersistentVolumeClaim", 20 | "Template", 21 | "ConfigMap", 22 | "Secret", 23 | "RoleBinding", 24 | "ServiceAccount", 25 | "CronJob", 26 | "Job", 27 | "LimitRange", 28 | "ResourceQuota", 29 | "HorizontalPodAutoscaler", 30 | "StatefulSet", 31 | } 32 | 33 | type ResourceFilter struct { 34 | Kinds []string 35 | Name string 36 | Label string 37 | ExcludedKinds []string 38 | ExcludedNames []string 39 | ExcludedLabels []string 40 | } 41 | 42 | // NewResourceFilter returns a filter based on kinds and flags. 43 | // kindArg might be blank, or a list of kinds (e.g. 'pvc,dc') or 44 | // a kind/name combination (e.g. 'dc/foo'). 45 | // selectorFlag might be blank or a key and a label, e.g. 'name=foo'. 46 | func NewResourceFilter(kindArg string, selectorFlag string, excludes []string) (*ResourceFilter, error) { 47 | filter := &ResourceFilter{ 48 | Kinds: []string{}, 49 | Name: "", 50 | Label: selectorFlag, 51 | } 52 | 53 | if len(kindArg) > 0 { 54 | kindArg = strings.ToLower(kindArg) 55 | 56 | if strings.Contains(kindArg, "/") { 57 | if strings.Contains(kindArg, ",") { 58 | return nil, errors.New( 59 | "You cannot target more than one resource name", 60 | ) 61 | } 62 | nameParts := strings.Split(kindArg, "/") 63 | filter.Name = KindMapping[nameParts[0]] + "/" + nameParts[1] 64 | return filter, nil 65 | } 66 | 67 | targetedKinds := make(map[string]bool) 68 | unknownKinds := []string{} 69 | kinds := strings.Split(kindArg, ",") 70 | for _, kind := range kinds { 71 | if _, ok := KindMapping[kind]; !ok { 72 | unknownKinds = append(unknownKinds, kind) 73 | } else { 74 | targetedKinds[KindMapping[kind]] = true 75 | } 76 | } 77 | 78 | if len(unknownKinds) > 0 { 79 | return nil, fmt.Errorf( 80 | "Unknown resource kinds: %s", 81 | strings.Join(unknownKinds, ","), 82 | ) 83 | } 84 | 85 | for kind := range targetedKinds { 86 | filter.Kinds = append(filter.Kinds, kind) 87 | } 88 | 89 | sort.Strings(filter.Kinds) 90 | } 91 | 92 | unknownKinds := []string{} 93 | for _, v := range excludes { 94 | v = strings.ToLower(v) 95 | if strings.Contains(v, "/") { // Name 96 | nameParts := strings.Split(v, "/") 97 | k := nameParts[0] 98 | if _, ok := KindMapping[k]; !ok { 99 | unknownKinds = append(unknownKinds, k) 100 | } else { 101 | filter.ExcludedNames = append(filter.ExcludedNames, KindMapping[k]+"/"+nameParts[1]) 102 | } 103 | } else if strings.Contains(v, "=") { // Label 104 | filter.ExcludedLabels = append(filter.ExcludedLabels, v) 105 | } else { // Kind 106 | if _, ok := KindMapping[v]; !ok { 107 | unknownKinds = append(unknownKinds, v) 108 | } else { 109 | filter.ExcludedKinds = append(filter.ExcludedKinds, KindMapping[v]) 110 | } 111 | } 112 | } 113 | 114 | if len(unknownKinds) > 0 { 115 | return nil, fmt.Errorf( 116 | "Unknown excluded resource kinds: %s", 117 | strings.Join(unknownKinds, ","), 118 | ) 119 | } 120 | 121 | return filter, nil 122 | } 123 | 124 | func (f *ResourceFilter) String() string { 125 | return fmt.Sprintf("Kinds: %s, Name: %s, Label: %s, ExcludedKinds: %s, ExcludedNames: %s, ExcludedLabels: %s", f.Kinds, f.Name, f.Label, f.ExcludedKinds, f.ExcludedNames, f.ExcludedLabels) 126 | } 127 | 128 | func (f *ResourceFilter) SatisfiedBy(item *ResourceItem) bool { 129 | if len(f.Name) > 0 && f.Name != item.FullName() { 130 | return false 131 | } 132 | 133 | if len(f.Kinds) > 0 && !utils.Includes(f.Kinds, item.Kind) { 134 | return false 135 | } 136 | 137 | if len(f.Label) > 0 { 138 | labels := strings.Split(f.Label, ",") 139 | for _, label := range labels { 140 | if !item.HasLabel(label) { 141 | return false 142 | } 143 | } 144 | } 145 | 146 | if len(f.ExcludedNames) > 0 { 147 | if utils.Includes(f.ExcludedNames, item.FullName()) { 148 | return false 149 | } 150 | } 151 | 152 | if len(f.ExcludedKinds) > 0 { 153 | if utils.Includes(f.ExcludedKinds, item.Kind) { 154 | return false 155 | } 156 | } 157 | 158 | if len(f.ExcludedLabels) > 0 { 159 | for _, el := range f.ExcludedLabels { 160 | if item.HasLabel(el) { 161 | return false 162 | } 163 | } 164 | } 165 | 166 | return true 167 | } 168 | 169 | func (f *ResourceFilter) ConvertToTarget() string { 170 | if len(f.Name) > 0 { 171 | return f.Name 172 | } 173 | kinds := f.Kinds 174 | if len(kinds) == 0 { 175 | kinds = availableKinds 176 | } 177 | return strings.Join(kinds, ",") 178 | } 179 | 180 | func (f *ResourceFilter) ConvertToKinds() string { 181 | if len(f.Name) > 0 { 182 | nameParts := strings.Split(f.Name, "/") 183 | return nameParts[0] 184 | } 185 | kinds := f.Kinds 186 | if len(kinds) == 0 { 187 | kinds = availableKinds 188 | } 189 | kindsWithoutExcluded := []string{} 190 | for _, k := range kinds { 191 | if !utils.Includes(f.ExcludedKinds, k) { 192 | kindsWithoutExcluded = append(kindsWithoutExcluded, k) 193 | } 194 | } 195 | return strings.Join(kindsWithoutExcluded, ",") 196 | } 197 | -------------------------------------------------------------------------------- /pkg/openshift/change_test.go: -------------------------------------------------------------------------------- 1 | package openshift 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestDiff(t *testing.T) { 9 | 10 | diffs := map[string]struct { 11 | currentAnnotations []byte 12 | currentData []byte 13 | desiredAnnotations []byte 14 | desiredData []byte 15 | expectedDiff string 16 | }{ 17 | "Modifying a data field": { 18 | currentAnnotations: []byte("{}"), 19 | currentData: []byte("{foo: bar}"), 20 | desiredAnnotations: []byte("{}"), 21 | desiredData: []byte("{foo: baz}"), 22 | expectedDiff: `--- Current State (OpenShift cluster) 23 | +++ Desired State (Processed template) 24 | @@ -1,6 +1,6 @@ 25 | apiVersion: v1 26 | data: 27 | - foo: bar 28 | + foo: baz 29 | kind: ConfigMap 30 | metadata: 31 | annotations: {} 32 | `, 33 | }, 34 | "Adding a data field": { 35 | currentAnnotations: []byte("{}"), 36 | currentData: []byte("{foo: bar}"), 37 | desiredAnnotations: []byte("{}"), 38 | desiredData: []byte("{foo: bar, baz: qux}"), 39 | expectedDiff: `--- Current State (OpenShift cluster) 40 | +++ Desired State (Processed template) 41 | @@ -1,5 +1,6 @@ 42 | apiVersion: v1 43 | data: 44 | + baz: qux 45 | foo: bar 46 | kind: ConfigMap 47 | metadata: 48 | `, 49 | }, 50 | "Removing a data field": { 51 | currentAnnotations: []byte("{}"), 52 | currentData: []byte("{foo: bar}"), 53 | desiredAnnotations: []byte("{}"), 54 | desiredData: []byte("{}"), 55 | expectedDiff: `--- Current State (OpenShift cluster) 56 | +++ Desired State (Processed template) 57 | @@ -1,6 +1,5 @@ 58 | apiVersion: v1 59 | -data: 60 | - foo: bar 61 | +data: {} 62 | kind: ConfigMap 63 | metadata: 64 | annotations: {} 65 | `, 66 | }, 67 | "Adding an annotation": { 68 | currentAnnotations: []byte("{}"), 69 | currentData: []byte("{}"), 70 | desiredAnnotations: []byte("{foo: bar}"), 71 | desiredData: []byte("{}"), 72 | expectedDiff: `--- Current State (OpenShift cluster) 73 | +++ Desired State (Processed template) 74 | @@ -2,7 +2,8 @@ 75 | data: {} 76 | kind: ConfigMap 77 | metadata: 78 | - annotations: {} 79 | + annotations: 80 | + foo: bar 81 | labels: 82 | app: bar 83 | name: bar 84 | `, 85 | }, 86 | "Removing an annotation": { 87 | currentAnnotations: []byte(`{foo: bar, kubectl.kubernetes.io/last-applied-configuration: '{"apiVersion":"v1","kind":"ConfigMap","metadata":{"annotations":{"foo":"bar"}}}'}`), 88 | currentData: []byte("{}"), 89 | desiredAnnotations: []byte("{}"), 90 | desiredData: []byte("{}"), 91 | expectedDiff: `--- Current State (OpenShift cluster) 92 | +++ Desired State (Processed template) 93 | @@ -2,8 +2,7 @@ 94 | data: {} 95 | kind: ConfigMap 96 | metadata: 97 | - annotations: 98 | - foo: bar 99 | + annotations: {} 100 | labels: 101 | app: bar 102 | name: bar 103 | `, 104 | }, 105 | "Modifying an annotation": { 106 | currentAnnotations: []byte(`{foo: bar, kubectl.kubernetes.io/last-applied-configuration: '{"apiVersion":"v1","kind":"ConfigMap","metadata":{"annotations":{"foo":"bar"}}}'}`), 107 | currentData: []byte("{}"), 108 | desiredAnnotations: []byte("{foo: baz}"), 109 | desiredData: []byte("{}"), 110 | expectedDiff: `--- Current State (OpenShift cluster) 111 | +++ Desired State (Processed template) 112 | @@ -3,7 +3,7 @@ 113 | kind: ConfigMap 114 | metadata: 115 | annotations: 116 | - foo: bar 117 | + foo: baz 118 | labels: 119 | app: bar 120 | name: bar 121 | `, 122 | }, 123 | "Modifying a non-managed annotation": { 124 | currentAnnotations: []byte(`{foo: bar, baz: qux, kubectl.kubernetes.io/last-applied-configuration: '{"apiVersion":"v1","kind":"ConfigMap","metadata":{"annotations":{"foo":"bar"}}}'}`), 125 | currentData: []byte("{}"), 126 | desiredAnnotations: []byte("{foo: bar, baz: zab}"), 127 | desiredData: []byte("{}"), 128 | expectedDiff: `--- Current State (OpenShift cluster) 129 | +++ Desired State (Processed template) 130 | @@ -3,7 +3,7 @@ 131 | kind: ConfigMap 132 | metadata: 133 | annotations: 134 | - baz: qux 135 | + baz: zab 136 | foo: bar 137 | labels: 138 | app: bar 139 | `, 140 | }, 141 | } 142 | 143 | for name, tt := range diffs { 144 | t.Run(name, func(t *testing.T) { 145 | currentItem := getItem( 146 | t, 147 | getConfigMapForDiff(tt.currentAnnotations, tt.currentData), 148 | "platform", 149 | ) 150 | desiredItem := getItem( 151 | t, 152 | getConfigMapForDiff(tt.desiredAnnotations, tt.desiredData), 153 | "template", 154 | ) 155 | changes, err := calculateChanges(desiredItem, currentItem, []string{}, true) 156 | if err != nil { 157 | t.Fatal(err) 158 | } 159 | change := changes[0] 160 | actualDiff := change.Diff(true) 161 | if actualDiff != tt.expectedDiff { 162 | t.Fatalf( 163 | "Diff()\n===== expected =====\n%s\n===== actual =====\n%s", 164 | tt.expectedDiff, 165 | actualDiff, 166 | ) 167 | } 168 | }) 169 | } 170 | } 171 | 172 | func getConfigMapForDiff(annotations, data []byte) []byte { 173 | config := []byte( 174 | `apiVersion: v1 175 | kind: ConfigMap 176 | metadata: 177 | labels: 178 | app: bar 179 | annotations: ANNOTATIONS 180 | name: bar 181 | data: DATA`) 182 | config = bytes.Replace(config, []byte("ANNOTATIONS"), annotations, -1) 183 | return bytes.Replace(config, []byte("DATA"), data, -1) 184 | } 185 | -------------------------------------------------------------------------------- /pkg/cli/oc_client.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | type ClientApplier interface { 12 | ClientProcessorExporter 13 | ClientModifier 14 | } 15 | 16 | // ClientProcessorExporter allows to process templates and export resources. 17 | type ClientProcessorExporter interface { 18 | OcClientProcessor 19 | OcClientExporter 20 | } 21 | 22 | // ClientModifier allows to delete and create/update resources. 23 | type ClientModifier interface { 24 | OcClientApplier 25 | OcClientDeleter 26 | } 27 | 28 | // OcClientProcessor is a stop-gap solution only ... should have a better API. 29 | type OcClientProcessor interface { 30 | Process(args []string) ([]byte, []byte, error) 31 | } 32 | 33 | // OcClientExporter allows to export resources. 34 | type OcClientExporter interface { 35 | Export(target string, label string) ([]byte, error) 36 | } 37 | 38 | // OcClientDeleter allows to delete a resource. 39 | type OcClientDeleter interface { 40 | Delete(kind string, name string) ([]byte, error) 41 | } 42 | 43 | // OcClientApplier allows to create/update a resource. 44 | type OcClientApplier interface { 45 | Apply(config string, selector string) ([]byte, error) 46 | } 47 | 48 | // OcClientVersioner allows to retrieve the OpenShift version.. 49 | type OcClientVersioner interface { 50 | Version() ([]byte, []byte, error) 51 | } 52 | 53 | // OcClient is a wrapper around the "oc" binary (client). 54 | type OcClient struct { 55 | namespace string 56 | } 57 | 58 | // NewOcClient creates a new ocClient. 59 | func NewOcClient(namespace string) *OcClient { 60 | return &OcClient{namespace: namespace} 61 | } 62 | 63 | // Version returns the output of "ov versiopn". 64 | func (c *OcClient) Version() ([]byte, []byte, error) { 65 | cmd := c.execPlainOcCmd([]string{"version"}) 66 | return c.runCmd(cmd) 67 | 68 | } 69 | 70 | // CurrentProject returns the currently active project name (namespace). 71 | func (c *OcClient) CurrentProject() (string, error) { 72 | cmd := c.execPlainOcCmd([]string{"project", "--short"}) 73 | n, err := cmd.CombinedOutput() 74 | return strings.TrimSpace(string(n)), err 75 | } 76 | 77 | // CheckProjectExists returns true if the given project (namespace) exists. 78 | func (c *OcClient) CheckProjectExists(p string) (bool, error) { 79 | cmd := c.execPlainOcCmd([]string{"project", p, "--short"}) 80 | _, err := cmd.CombinedOutput() 81 | return err == nil, err 82 | } 83 | 84 | // CheckLoggedIn returns true if the given project (namespace) exists. 85 | func (c *OcClient) CheckLoggedIn() (bool, error) { 86 | cmd := exec.Command(ocBinary, "whoami") 87 | _, err := cmd.CombinedOutput() 88 | return err == nil, err 89 | } 90 | 91 | // Process processes an OpenShift template. 92 | // The API is just a stop-gap solution and will be better in the future. 93 | func (c *OcClient) Process(args []string) ([]byte, []byte, error) { 94 | processArgs := append([]string{"process"}, args...) 95 | cmd := c.execPlainOcCmd(processArgs) 96 | return c.runCmd(cmd) 97 | } 98 | 99 | // Export exports resources from OpenShift as a template. 100 | func (c *OcClient) Export(target string, label string) ([]byte, error) { 101 | args := []string{"get", target, "--output=yaml"} 102 | cmd := c.execOcCmd( 103 | args, 104 | c.namespace, 105 | label, 106 | ) 107 | outBytes, errBytes, err := c.runCmd(cmd) 108 | 109 | if err != nil { 110 | ret := string(errBytes) 111 | 112 | if strings.Contains(ret, "no resources found") { 113 | return []byte{}, nil 114 | } 115 | 116 | return []byte{}, fmt.Errorf( 117 | "Failed to export %s resources.\n"+ 118 | "%s\n", 119 | target, 120 | ret, 121 | ) 122 | } 123 | 124 | return outBytes, nil 125 | } 126 | 127 | // Apply applies given resource configuration. 128 | func (c *OcClient) Apply(config string, selector string) ([]byte, error) { 129 | args := []string{"apply", "-f", "-"} 130 | cmd := c.execOcCmd( 131 | args, 132 | c.namespace, 133 | selector, 134 | ) 135 | stdin, err := cmd.StdinPipe() 136 | if err != nil { 137 | return nil, err 138 | } 139 | go func() { 140 | defer stdin.Close() 141 | _, _ = io.WriteString(stdin, config) 142 | }() 143 | _, errBytes, err := c.runCmd(cmd) 144 | return errBytes, err 145 | } 146 | 147 | // Delete deletes given resource. 148 | func (c *OcClient) Delete(kind string, name string) ([]byte, error) { 149 | args := []string{"delete", kind, name} 150 | cmd := c.execOcCmd( 151 | args, 152 | c.namespace, 153 | "", // empty as name and selector is not allowed 154 | ) 155 | _, errBytes, err := c.runCmd(cmd) 156 | return errBytes, err 157 | } 158 | 159 | func (c *OcClient) execOcCmd(args []string, namespace string, selector string) *exec.Cmd { 160 | if len(namespace) > 0 { 161 | args = append(args, "--namespace="+namespace) 162 | } 163 | if len(selector) > 0 { 164 | args = append(args, "--selector="+selector) 165 | } 166 | return c.execPlainOcCmd(args) 167 | } 168 | 169 | func (c *OcClient) execPlainOcCmd(args []string) *exec.Cmd { 170 | return c.execCmd(ocBinary, args) 171 | } 172 | 173 | func (c *OcClient) execCmd(executable string, args []string) *exec.Cmd { 174 | if verbose { 175 | PrintBluef("--> %s\n", executable+" "+strings.Join(args, " ")) 176 | } 177 | return exec.Command(executable, args...) 178 | } 179 | 180 | func (c *OcClient) runCmd(cmd *exec.Cmd) (outBytes, errBytes []byte, err error) { 181 | var stdout, stderr bytes.Buffer 182 | cmd.Stdout = &stdout 183 | cmd.Stderr = &stderr 184 | err = cmd.Run() 185 | outBytes = stdout.Bytes() 186 | errBytes = stderr.Bytes() 187 | return outBytes, errBytes, err 188 | } 189 | -------------------------------------------------------------------------------- /pkg/commands/secrets.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/opendevstack/tailor/pkg/cli" 11 | "github.com/opendevstack/tailor/pkg/openshift" 12 | "github.com/opendevstack/tailor/pkg/utils" 13 | ) 14 | 15 | // GenerateKey generates a GPG key using specified email (and optionally name). 16 | func GenerateKey(secretsOptions *cli.SecretsOptions, email, name string) error { 17 | emailParts := strings.Split(email, "@") 18 | if len(name) == 0 { 19 | name = emailParts[0] 20 | } 21 | entity, err := utils.CreateEntity(name, email) 22 | if err != nil { 23 | return fmt.Errorf("Failed to generate keypair: %s", err) 24 | } 25 | publicKeyFilename := strings.Replace(emailParts[0], ".", "-", -1) + ".key" 26 | if _, err := os.Stat(publicKeyFilename); err == nil { 27 | return fmt.Errorf("'%s' already exists", publicKeyFilename) 28 | } 29 | err = utils.PrintPublicKey(entity, publicKeyFilename) 30 | if err != nil { 31 | return err 32 | } 33 | fmt.Printf("Public Key written to %s. This file can be committed.\n", publicKeyFilename) 34 | privateKeyFilename := secretsOptions.PrivateKey 35 | if _, err := os.Stat(privateKeyFilename); err == nil { 36 | return fmt.Errorf("'%s' already exists", privateKeyFilename) 37 | } 38 | err = utils.PrintPrivateKey(entity, privateKeyFilename) 39 | if err != nil { 40 | return err 41 | } 42 | fmt.Printf("Private Key written to %s. This file MUST NOT be committed.\n", privateKeyFilename) 43 | return nil 44 | } 45 | 46 | // Reveal prints the clear-text of an encrypted file to STDOUT. 47 | func Reveal(secretsOptions *cli.SecretsOptions, filename string) error { 48 | if _, err := os.Stat(filename); os.IsNotExist(err) { 49 | return fmt.Errorf("'%s' does not exist", filename) 50 | } 51 | encryptedContent, err := utils.ReadFile(filename) 52 | if err != nil { 53 | return fmt.Errorf("Could not read file: %s", err) 54 | } 55 | decryptedContent, err := openshift.DecryptedParams( 56 | encryptedContent, 57 | secretsOptions.PrivateKey, 58 | secretsOptions.Passphrase, 59 | ) 60 | if err != nil { 61 | return fmt.Errorf("Could not decrypt file: %s", err) 62 | } 63 | fmt.Println(decryptedContent) 64 | return nil 65 | } 66 | 67 | // ReEncrypt decrypts given file(s) and encrypts all params again. 68 | // This allows to share the secrets with a new keypair. 69 | func ReEncrypt(secretsOptions *cli.SecretsOptions, filename string) error { 70 | if len(filename) > 0 { 71 | err := reEncrypt(filename, secretsOptions.PrivateKey, secretsOptions.Passphrase, secretsOptions.PublicKeyDir) 72 | if err != nil { 73 | return err 74 | } 75 | } else { 76 | paramDir := secretsOptions.ParamDir 77 | files, err := ioutil.ReadDir(paramDir) 78 | if err != nil { 79 | return err 80 | } 81 | filePattern := ".*\\.env.enc$" 82 | re := regexp.MustCompile(filePattern) 83 | for _, file := range files { 84 | matched := re.MatchString(file.Name()) 85 | if !matched { 86 | continue 87 | } 88 | filename := paramDir + string(os.PathSeparator) + file.Name() 89 | err := reEncrypt(filename, secretsOptions.PrivateKey, secretsOptions.Passphrase, secretsOptions.PublicKeyDir) 90 | if err != nil { 91 | return err 92 | } 93 | } 94 | } 95 | return nil 96 | } 97 | 98 | // Edit opens given filen in cleartext in $EDITOR, then encrypts the content on save. 99 | func Edit(secretsOptions *cli.SecretsOptions, filename string) error { 100 | encryptedContent, err := utils.ReadFile(filename) 101 | if err != nil { 102 | if os.IsNotExist(err) { 103 | cli.DebugMsg(filename, "does not exist, creating empty file") 104 | } else { 105 | return fmt.Errorf("Could not read file: %s", err) 106 | } 107 | } 108 | 109 | cleartextContent, err := openshift.DecryptedParams( 110 | encryptedContent, 111 | secretsOptions.PrivateKey, 112 | secretsOptions.Passphrase, 113 | ) 114 | if err != nil { 115 | return fmt.Errorf("Could not decrypt file: %s", err) 116 | } 117 | 118 | editedContent, err := cli.EditEnvFile(cleartextContent) 119 | if err != nil { 120 | return fmt.Errorf("Could not edit file: %s", err) 121 | } 122 | 123 | err = writeEncryptedContent( 124 | filename, 125 | editedContent, 126 | encryptedContent, 127 | secretsOptions.PrivateKey, 128 | secretsOptions.Passphrase, 129 | secretsOptions.PublicKeyDir, 130 | ) 131 | if err != nil { 132 | return fmt.Errorf("Could not write file: %s", err) 133 | } 134 | return nil 135 | } 136 | 137 | func reEncrypt(filename, privateKey, passphrase, publicKeyDir string) error { 138 | encryptedContent, err := utils.ReadFile(filename) 139 | if err != nil { 140 | return fmt.Errorf("Could not read file: %s", err) 141 | } 142 | 143 | cleartextContent, err := openshift.DecryptedParams( 144 | encryptedContent, 145 | privateKey, 146 | passphrase, 147 | ) 148 | if err != nil { 149 | return fmt.Errorf("Could not decrypt file: %s", err) 150 | } 151 | 152 | return writeEncryptedContent( 153 | filename, 154 | cleartextContent, 155 | "", // empty because all values should be re-encrypted 156 | privateKey, 157 | passphrase, 158 | publicKeyDir, 159 | ) 160 | } 161 | 162 | func writeEncryptedContent(filename, newContent, previousContent, privateKey, passphrase, publicKeyDir string) error { 163 | updatedContent, err := openshift.EncryptedParams( 164 | newContent, 165 | previousContent, 166 | publicKeyDir, 167 | privateKey, 168 | passphrase, 169 | ) 170 | if err != nil { 171 | return fmt.Errorf("Could not encrypt content: %s", err) 172 | } 173 | 174 | err = ioutil.WriteFile(filename, []byte(updatedContent), 0644) 175 | if err != nil { 176 | return fmt.Errorf("Could not write file: %s", err) 177 | } 178 | return nil 179 | } 180 | -------------------------------------------------------------------------------- /pkg/openshift/template_test.go: -------------------------------------------------------------------------------- 1 | package openshift 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | "github.com/opendevstack/tailor/internal/test/helper" 8 | "github.com/opendevstack/tailor/pkg/cli" 9 | "github.com/opendevstack/tailor/pkg/utils" 10 | ) 11 | 12 | func TestTemplateContainsTailorNamespaceParam(t *testing.T) { 13 | tests := map[string]struct { 14 | filename string 15 | wantContains bool 16 | wantError string 17 | }{ 18 | "contains param": { 19 | filename: "with-tailor-namespace-param.yml", 20 | wantContains: true, 21 | wantError: "", 22 | }, 23 | "without param": { 24 | filename: "without-tailor-namespace-param.yml", 25 | wantContains: false, 26 | wantError: "", 27 | }, 28 | "invalid template": { 29 | filename: "invalid-template.yml", 30 | wantContains: false, 31 | wantError: "Not a valid template. Did you forget to add the template header?\n\napiVersion: v1\nkind: Template\nobjects: [...]", 32 | }, 33 | "template with blank parameters": { 34 | filename: "template-blank-parameters.yml", 35 | wantContains: false, 36 | wantError: "", 37 | }, 38 | "garbage": { 39 | filename: "garbage.yml", 40 | wantContains: false, 41 | wantError: "Not a valid template. Please see https://github.com/opendevstack/tailor#template-authoring", 42 | }, 43 | } 44 | for name, tc := range tests { 45 | t.Run(name, func(t *testing.T) { 46 | contains, err := templateContainsTailorNamespaceParam( 47 | "../../internal/test/fixtures/template-param-detection/" + tc.filename, 48 | ) 49 | if len(tc.wantError) == 0 { 50 | if err != nil { 51 | t.Fatalf("Could not determine if the template contains the param: %s", err) 52 | } 53 | } else { 54 | if err == nil { 55 | t.Fatalf("Want error '%s', but no error occured", tc.wantError) 56 | } 57 | if tc.wantError != err.Error() { 58 | t.Fatalf("Want error '%s', got '%s'", tc.wantError, err) 59 | } 60 | } 61 | if tc.wantContains != contains { 62 | t.Fatalf("Want template containing param '%t', got '%t'", tc.wantContains, contains) 63 | } 64 | }) 65 | } 66 | } 67 | 68 | func TestCalculateParamFiles(t *testing.T) { 69 | tests := map[string]struct { 70 | namespace string 71 | templateName string 72 | paramDir string 73 | paramFileFlag []string 74 | fs utils.FileStater 75 | expected []string 76 | }{ 77 | "template is foo.yml and corresponding param file exists": { 78 | namespace: "foo", 79 | templateName: "bar.yml", 80 | paramDir: ".", // default 81 | paramFileFlag: []string{}, 82 | fs: &helper.SomeFilesExistFS{Existing: []string{"bar.env", "foo.env"}}, 83 | expected: []string{"bar.env", "foo.env"}, 84 | }, 85 | "template is bar.yml but corresponding param file does not exist": { 86 | namespace: "foo", 87 | templateName: "bar.yml", 88 | paramDir: ".", // default 89 | paramFileFlag: []string{}, 90 | fs: &helper.SomeFilesExistFS{Existing: []string{"foo.env"}}, 91 | expected: []string{"foo.env"}, 92 | }, 93 | "template is bar.yml and no files exist": { 94 | namespace: "foo", 95 | templateName: "bar.yml", 96 | paramDir: ".", // default 97 | paramFileFlag: []string{}, 98 | fs: &helper.SomeFilesExistFS{}, 99 | expected: []string{}, 100 | }, 101 | "template is foo.yml and corresponding param file exists in param dir": { 102 | namespace: "foo", 103 | templateName: "bar.yml", 104 | paramDir: "foo", // default 105 | paramFileFlag: []string{}, 106 | fs: &helper.SomeFilesExistFS{Existing: []string{"foo/bar.env", "foo.env"}}, 107 | expected: []string{"foo/bar.env", "foo.env"}, 108 | }, 109 | "template is foo.yml but corresponding param file does not exist in param dir": { 110 | namespace: "foo", 111 | templateName: "bar.yml", 112 | paramDir: "foo", // default 113 | paramFileFlag: []string{}, 114 | fs: &helper.SomeFilesExistFS{Existing: []string{"foo", "foo.env"}}, 115 | expected: []string{"foo.env"}, 116 | }, 117 | "param env file is given explicitly": { 118 | namespace: "foo", 119 | templateName: "bar.yml", 120 | paramDir: ".", // default 121 | paramFileFlag: []string{"foo.env"}, 122 | fs: &helper.SomeFilesExistFS{Existing: []string{"foo.env"}}, 123 | expected: []string{"foo.env"}, 124 | }, 125 | } 126 | for name, tc := range tests { 127 | t.Run(name, func(t *testing.T) { 128 | globalOptions := cli.InitGlobalOptions(tc.fs) 129 | compareOptions := &cli.CompareOptions{ 130 | GlobalOptions: globalOptions, 131 | NamespaceOptions: &cli.NamespaceOptions{Namespace: tc.namespace}, 132 | ParamFiles: tc.paramFileFlag, 133 | } 134 | 135 | actual := calculateParamFiles(tc.templateName, tc.paramDir, compareOptions) 136 | if diff := cmp.Diff(tc.expected, actual); diff != "" { 137 | t.Fatalf("Desired state mismatch (-want +got):\n%s", diff) 138 | } 139 | }) 140 | } 141 | } 142 | 143 | func TestReadParamFileBytes(t *testing.T) { 144 | tests := map[string]struct { 145 | paramFiles []string 146 | expected string 147 | }{ 148 | "multiple files get concatenated": { 149 | paramFiles: []string{"foo.env", "bar.env"}, 150 | expected: "FOO=foo\nBAR=bar\n", 151 | }, 152 | "missing EOL is handled in concatenation": { 153 | paramFiles: []string{"baz-without-eol.env", "bar.env"}, 154 | expected: "BAZ=baz\nBAR=bar\n", 155 | }, 156 | } 157 | for name, tc := range tests { 158 | t.Run(name, func(t *testing.T) { 159 | actualParamFiles := []string{} 160 | for _, f := range tc.paramFiles { 161 | actualParamFiles = append(actualParamFiles, "../../internal/test/fixtures/param-files/"+f) 162 | } 163 | b, err := readParamFileBytes(actualParamFiles, "", "") 164 | if err != nil { 165 | t.Fatal(err) 166 | } 167 | got := string(b) 168 | if diff := cmp.Diff(tc.expected, got); diff != "" { 169 | t.Fatalf("Result is not expected (-want +got):\n%s", diff) 170 | } 171 | }) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /pkg/openshift/template.go: -------------------------------------------------------------------------------- 1 | package openshift 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "strings" 10 | 11 | "github.com/ghodss/yaml" 12 | "github.com/opendevstack/tailor/pkg/cli" 13 | "github.com/opendevstack/tailor/pkg/utils" 14 | "github.com/xeipuuv/gojsonpointer" 15 | ) 16 | 17 | // ProcessTemplate processes template "name" in "templateDir". 18 | func ProcessTemplate(templateDir string, name string, paramDir string, compareOptions *cli.CompareOptions, ocClient cli.OcClientProcessor) ([]byte, error) { 19 | filename := templateDir + string(os.PathSeparator) + name 20 | 21 | args := []string{"--filename=" + filename, "--output=yaml"} 22 | 23 | if len(compareOptions.Labels) > 0 { 24 | args = append(args, "--labels="+compareOptions.Labels) 25 | } 26 | 27 | for _, param := range compareOptions.Params { 28 | args = append(args, "--param="+param) 29 | } 30 | containsNamespace, err := templateContainsTailorNamespaceParam(filename) 31 | if err != nil { 32 | return []byte{}, err 33 | } 34 | if containsNamespace { 35 | args = append(args, "--param=TAILOR_NAMESPACE="+compareOptions.Namespace) 36 | } 37 | 38 | actualParamFiles := calculateParamFiles(name, paramDir, compareOptions) 39 | 40 | // Now turn the param files into arguments for the oc binary 41 | if len(actualParamFiles) > 0 { 42 | paramFileBytes, err := readParamFileBytes( 43 | actualParamFiles, 44 | compareOptions.PrivateKey, 45 | compareOptions.Passphrase, 46 | ) 47 | if err != nil { 48 | return []byte{}, err 49 | } 50 | 51 | tempParamFile, err := ioutil.TempFile("", ".combined.*.env") 52 | if err != nil { 53 | return []byte{}, err 54 | } 55 | 56 | defer os.Remove(tempParamFile.Name()) 57 | 58 | cli.DebugMsg("Writing contents of param files into", tempParamFile.Name()) 59 | _, err = tempParamFile.Write(paramFileBytes) 60 | if err != nil { 61 | tempParamFile.Close() 62 | return []byte{}, err 63 | } 64 | tempParamFile.Close() 65 | 66 | args = append(args, "--param-file="+tempParamFile.Name()) 67 | } 68 | 69 | if compareOptions.IgnoreUnknownParameters { 70 | args = append(args, "--ignore-unknown-parameters=true") 71 | } 72 | outBytes, errBytes, err := ocClient.Process(args) 73 | 74 | if len(errBytes) > 0 { 75 | fmt.Println(string(errBytes)) 76 | } 77 | if err != nil { 78 | return []byte{}, err 79 | } 80 | 81 | cli.DebugMsg("Processed template:", filename) 82 | return outBytes, err 83 | } 84 | 85 | // Returns true if template contains a param like "name: TAILOR_NAMESPACE" 86 | func templateContainsTailorNamespaceParam(filename string) (bool, error) { 87 | b, err := ioutil.ReadFile(filename) 88 | if err != nil { 89 | return false, fmt.Errorf("Could not read file '%s': %s", filename, err) 90 | } 91 | var f interface{} 92 | err = yaml.Unmarshal(b, &f) 93 | if err != nil { 94 | err = utils.DisplaySyntaxError(b, err) 95 | return false, err 96 | } 97 | var m map[string]interface{} 98 | switch f := f.(type) { 99 | case map[string]interface{}: 100 | m = f 101 | case []interface{}: 102 | return false, errors.New("Not a valid template. Did you forget to add the template header?\n\napiVersion: v1\nkind: Template\nobjects: [...]") 103 | default: 104 | return false, errors.New("Not a valid template. Please see https://github.com/opendevstack/tailor#template-authoring") 105 | } 106 | parametersPointer, _ := gojsonpointer.NewJsonPointer("/parameters") 107 | parameters, _, err := parametersPointer.Get(m) 108 | if err != nil || parameters == nil { 109 | return false, nil 110 | } 111 | for _, v := range parameters.([]interface{}) { 112 | nameVal := v.(map[string]interface{})["name"] 113 | if nameVal == nil { 114 | return false, errors.New("Template parameter without 'name' property found") 115 | } 116 | paramName := strings.TrimSpace(nameVal.(string)) 117 | if paramName == "TAILOR_NAMESPACE" { 118 | return true, nil 119 | } 120 | } 121 | return false, nil 122 | } 123 | 124 | func calculateParamFiles(name string, paramDir string, compareOptions *cli.CompareOptions) []string { 125 | files := compareOptions.ParamFiles 126 | // If param-file is not given, we assume a param-dir 127 | if len(files) == 0 { 128 | // Prefer folder over current directory 129 | if paramDir == "." { 130 | if _, err := os.Stat(compareOptions.Namespace); err == nil { 131 | paramDir = compareOptions.Namespace 132 | } 133 | } 134 | 135 | cli.DebugMsg(fmt.Sprintf("Looking for param files in '%s'", paramDir)) 136 | 137 | fileParts := strings.Split(name, ".") 138 | fileParts[len(fileParts)-1] = "env" 139 | f := strings.Join(fileParts, ".") 140 | if paramDir != "." { 141 | f = paramDir + string(os.PathSeparator) + f 142 | } 143 | if compareOptions.FileExists(f) { 144 | files = []string{f} 145 | } 146 | } 147 | // Add .env file if it exists 148 | namespaceDotEnvFile := fmt.Sprintf("%s.env", compareOptions.Namespace) 149 | if !utils.Includes(files, namespaceDotEnvFile) { 150 | if compareOptions.FileExists(namespaceDotEnvFile) { 151 | cli.DebugMsg(fmt.Sprintf("Adding param file '%s' by convention", namespaceDotEnvFile)) 152 | files = append(files, namespaceDotEnvFile) 153 | } 154 | } 155 | return files 156 | } 157 | 158 | func readParamFileBytes(paramFiles []string, privateKey string, passphrase string) ([]byte, error) { 159 | paramFileBytes := []byte{} 160 | for _, f := range paramFiles { 161 | cli.DebugMsg("Reading content of param file", f) 162 | b, err := ioutil.ReadFile(f) 163 | if err != nil { 164 | return []byte{}, err 165 | } 166 | eol := []byte("\n") 167 | if !bytes.HasSuffix(b, eol) { 168 | b = append(b, eol...) 169 | } 170 | paramFileBytes = append(paramFileBytes, b...) 171 | // Check if encrypted param file exists, and if so, decrypt and 172 | // append its content 173 | encFile := f + ".enc" 174 | if _, err := os.Stat(encFile); err == nil { 175 | cli.DebugMsg("Reading content of encrypted param file", encFile) 176 | b, err := ioutil.ReadFile(encFile) 177 | if err != nil { 178 | return []byte{}, err 179 | } 180 | encoded, err := EncodedParams(string(b), privateKey, passphrase) 181 | if err != nil { 182 | return []byte{}, err 183 | } 184 | paramFileBytes = append(paramFileBytes, []byte(encoded)...) 185 | } 186 | } 187 | return paramFileBytes, nil 188 | } 189 | --------------------------------------------------------------------------------