build
++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 46c733331..6ae40730b 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v2 - name: Lint - run: ./travis/kyaml-pre-commit.sh + run: ./scripts/kyaml-pre-commit.sh env: KUSTOMIZE_DOCKER_E2E: false # don't need to do e2e tests for linting diff --git a/Makefile b/Makefile index cf299c7bc..76b49a82e 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,17 @@ MYGOBIN := $(shell go env GOPATH)/bin SHELL := /bin/bash export PATH := $(MYGOBIN):$(PATH) +MODULES := '"cmd/config" "api/" "kustomize/" "kyaml/"' + +# Provide defaults for REPO_OWNER and REPO_NAME if not present. +# Typically these values would be provided by Prow. +ifndef REPO_OWNER +REPO_OWNER := "kubernetes-sigs" +endif + +ifndef REPO_NAME +REPO_NAME := "kustomize" +endif .PHONY: all all: verify-kustomize @@ -15,8 +26,7 @@ verify-kustomize: \ lint-kustomize \ test-unit-kustomize-all \ test-examples-kustomize-against-HEAD \ - test-examples-kustomize-against-3.8.0 \ - test-examples-kustomize-against-3.8.2 + test-examples-kustomize-against-3.8.6 # The following target referenced by a file in # https://github.com/kubernetes/test-infra/tree/master/config/jobs/kubernetes-sigs/kustomize @@ -27,8 +37,11 @@ prow-presubmit-check: \ test-unit-cmd-all \ test-go-mod \ test-examples-kustomize-against-HEAD \ - test-examples-kustomize-against-3.8.0 \ - test-examples-kustomize-against-3.8.2 + test-examples-kustomize-against-3.8.6 + +# test-multi-module \ +# Temporarily removed from prow-presubmit-check +# See https://github.com/kubernetes-sigs/kustomize/issues/3191 .PHONY: verify-kustomize-e2e verify-kustomize-e2e: test-examples-e2e-kustomize @@ -91,7 +104,8 @@ install-tools: \ $(MYGOBIN)/mdrip \ $(MYGOBIN)/pluginator \ $(MYGOBIN)/prchecker \ - $(MYGOBIN)/stringer + $(MYGOBIN)/stringer \ + $(MYGOBIN)/helm ### Begin kustomize plugin rules. # @@ -133,7 +147,8 @@ _builtinplugins = \ PrefixSuffixTransformer.go \ ReplicaCountTransformer.go \ SecretGenerator.go \ - ValueAddTransformer.go + ValueAddTransformer.go \ + HelmChartInflationGenerator.go # Maintaining this explicit list of generated files, and # adding it as a dependency to a few targets, to assure @@ -159,6 +174,7 @@ $(pGen)/PrefixSuffixTransformer.go: $(pSrc)/prefixsuffixtransformer/PrefixSuffix $(pGen)/ReplicaCountTransformer.go: $(pSrc)/replicacounttransformer/ReplicaCountTransformer.go $(pGen)/SecretGenerator.go: $(pSrc)/secretgenerator/SecretGenerator.go $(pGen)/ValueAddTransformer.go: $(pSrc)/valueaddtransformer/ValueAddTransformer.go +$(pGen)/HelmChartInflationGenerator.go: $(pSrc)/helmchartinflationgenerator/HelmChartInflationGenerator.go # The (verbose but portable) Makefile way to convert to lowercase. toLowerCase = $(subst A,a,$(subst B,b,$(subst C,c,$(subst D,d,$(subst E,e,$(subst F,f,$(subst G,g,$(subst H,h,$(subst I,i,$(subst J,j,$(subst K,k,$(subst L,l,$(subst M,m,$(subst N,n,$(subst O,o,$(subst P,p,$(subst Q,q,$(subst R,r,$(subst S,s,$(subst T,t,$(subst U,u,$(subst V,v,$(subst W,w,$(subst X,x,$(subst Y,y,$(subst Z,z,$1)))))))))))))))))))))))))) @@ -221,10 +237,23 @@ test-unit-kustomize-all: \ test-unit-kustomize-plugins test-unit-cmd-all: - ./travis/kyaml-pre-commit.sh + ./scripts/kyaml-pre-commit.sh test-go-mod: - ./travis/check-go-mod.sh + ./scripts/check-go-mod.sh + +# Environment variables are defined at +# https://github.com/kubernetes/test-infra/blob/master/prow/jobs.md#job-environment-variables +.PHONY: test-multi-module +test-multi-module: $(MYGOBIN)/prchecker + ( \ + export MYGOBIN=$(MYGOBIN); \ + export REPO_OWNER=$(REPO_OWNER); \ + export REPO_NAME=$(REPO_NAME); \ + export PULL_NUMBER=$(PULL_NUMBER); \ + export MODULES=$(MODULES); \ + ./scripts/check-multi-module.sh; \ + ) .PHONY: test-examples-e2e-kustomize: $(MYGOBIN)/mdrip $(MYGOBIN)/kind @@ -241,23 +270,10 @@ test-examples-kustomize-against-HEAD: $(MYGOBIN)/kustomize $(MYGOBIN)/mdrip ./hack/testExamplesAgainstKustomize.sh HEAD .PHONY: -test-examples-kustomize-against-3.8.0: $(MYGOBIN)/mdrip +test-examples-kustomize-against-3.8.6: $(MYGOBIN)/mdrip ( \ set -e; \ - tag=v3.8.0; \ - /bin/rm -f $(MYGOBIN)/kustomize; \ - echo "Installing kustomize $$tag."; \ - GO111MODULE=on go get sigs.k8s.io/kustomize/kustomize/v3@$${tag}; \ - ./hack/testExamplesAgainstKustomize.sh $$tag; \ - echo "Reinstalling kustomize from HEAD."; \ - cd kustomize; go install .; \ - ) - -.PHONY: -test-examples-kustomize-against-3.8.2: $(MYGOBIN)/mdrip - ( \ - set -e; \ - tag=v3.8.2; \ + tag=v3.8.6; \ /bin/rm -f $(MYGOBIN)/kustomize; \ echo "Installing kustomize $$tag."; \ GO111MODULE=on go get sigs.k8s.io/kustomize/kustomize/v3@$${tag}; \ @@ -304,16 +320,16 @@ $(MYGOBIN)/helmV3: ( \ set -e; \ d=$(shell mktemp -d); cd $$d; \ - tgzFile=helm-v3.2.0-rc.1-linux-amd64.tar.gz; \ + tgzFile=helm-v3.4.0-linux-amd64.tar.gz; \ wget https://get.helm.sh/$$tgzFile; \ tar -xvzf $$tgzFile; \ mv linux-amd64/helm $(MYGOBIN)/helmV3; \ rm -rf $$d \ ) -# Default version of helm is v2 for the time being. -$(MYGOBIN)/helm: $(MYGOBIN)/helmV2 - ln -s $(MYGOBIN)/helmV2 $(MYGOBIN)/helm +# Default version of helm is v3. +$(MYGOBIN)/helm: $(MYGOBIN)/helmV3 + ln -s $(MYGOBIN)/helmV3 $(MYGOBIN)/helm $(MYGOBIN)/kind: ( \ diff --git a/api/builtins/HelmChartInflationGenerator.go b/api/builtins/HelmChartInflationGenerator.go new file mode 100644 index 000000000..730fe824b --- /dev/null +++ b/api/builtins/HelmChartInflationGenerator.go @@ -0,0 +1,178 @@ +// Code generated by pluginator on HelmChartInflationGenerator; DO NOT EDIT. +// pluginator {unknown 1970-01-01T00:00:00Z } + +package builtins + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path" + "regexp" + "strings" + + "github.com/pkg/errors" + "sigs.k8s.io/kustomize/api/filesys" + "sigs.k8s.io/kustomize/api/resmap" + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/yaml" +) + +// HelmChartInflationGeneratorPlugin is a plugin to generate resources +// from a remote or local helm chart. +type HelmChartInflationGeneratorPlugin struct { + h *resmap.PluginHelpers + types.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + runHelmCommand func([]string) ([]byte, error) + types.HelmChartArgs + tmpDir string +} + +var KustomizePlugin HelmChartInflationGeneratorPlugin + +// Config uses the input plugin configurations `config` to setup the generator +// options +func (p *HelmChartInflationGeneratorPlugin) Config(h *resmap.PluginHelpers, config []byte) error { + p.h = h + err := yaml.Unmarshal(config, p) + if err != nil { + return err + } + tmpDir, err := filesys.NewTmpConfirmedDir() + if err != nil { + return err + } + p.tmpDir = string(tmpDir) + if p.ChartName == "" { + return fmt.Errorf("chartName cannot be empty") + } + if p.ChartHome == "" { + p.ChartHome = path.Join(p.tmpDir, "chart") + } + if p.ChartRepoName == "" { + p.ChartRepoName = "stable" + } + if p.HelmBin == "" { + p.HelmBin = "helm" + } + if p.HelmHome == "" { + p.HelmHome = path.Join(p.tmpDir, ".helm") + } + if p.Values == "" { + p.Values = path.Join(p.ChartHome, p.ChartName, "values.yaml") + } + // runHelmCommand will run `helm` command with args provided. Return stdout + // and error if there is any. + p.runHelmCommand = func(args []string) ([]byte, error) { + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + cmd := exec.Command(p.HelmBin, args...) + cmd.Stdout = stdout + cmd.Stderr = stderr + cmd.Env = append(cmd.Env, + fmt.Sprintf("HELM_CONFIG_HOME=%s", p.HelmHome), + fmt.Sprintf("HELM_CACHE_HOME=%s/.cache", p.HelmHome), + fmt.Sprintf("HELM_DATA_HOME=%s/.data", p.HelmHome), + ) + err := cmd.Run() + if err != nil { + return stdout.Bytes(), + errors.Wrap( + fmt.Errorf("failed to run command %s %s", p.HelmBin, strings.Join(args, " ")), + stderr.String(), + ) + } + return stdout.Bytes(), nil + } + return nil +} + +// Generate implements generator +func (p *HelmChartInflationGeneratorPlugin) Generate() (resmap.ResMap, error) { + // cleanup + defer os.RemoveAll(p.tmpDir) + // check helm version. we only support V3 + err := p.checkHelmVersion() + if err != nil { + return nil, err + } + // pull the chart + if !p.checkLocalChart() { + _, err := p.runHelmCommand(p.getPullCommandArgs()) + if err != nil { + return nil, err + } + } + // render the charts + stdout, err := p.runHelmCommand(p.getTemplateCommandArgs()) + if err != nil { + return nil, err + } + + return p.h.ResmapFactory().NewResMapFromBytes(stdout) +} + +func (p *HelmChartInflationGeneratorPlugin) getTemplateCommandArgs() []string { + args := []string{"template"} + if p.ReleaseName != "" { + args = append(args, p.ReleaseName) + } + args = append(args, path.Join(p.ChartHome, p.ChartName)) + if p.ReleaseNamespace != "" { + args = append(args, "--namespace", p.ReleaseNamespace) + } + if p.Values != "" { + args = append(args, "--values", p.Values) + } + return args +} + +func (p *HelmChartInflationGeneratorPlugin) getPullCommandArgs() []string { + args := []string{"pull", "--untar", "--untardir", p.ChartHome} + chartName := fmt.Sprintf("%s/%s", p.ChartRepoName, p.ChartName) + if p.ChartVersion != "" { + args = append(args, "--version", p.ChartVersion) + } + if p.ChartRepoURL != "" { + args = append(args, "--repo", p.ChartRepoURL) + chartName = p.ChartName + } + + args = append(args, chartName) + + return args +} + +// checkLocalChart will return true if the chart does exist in +// local chart home. +func (p *HelmChartInflationGeneratorPlugin) checkLocalChart() bool { + path := path.Join(p.ChartHome, p.ChartName) + s, err := os.Stat(path) + if err != nil { + return false + } + return s.IsDir() +} + +// checkHelmVersion will return an error if the helm version is not V3 +func (p *HelmChartInflationGeneratorPlugin) checkHelmVersion() error { + stdout, err := p.runHelmCommand([]string{"version", "-c", "--short"}) + if err != nil { + return err + } + r, err := regexp.Compile(`v\d+(\.\d+)+`) + if err != nil { + return err + } + v := string(r.Find(stdout))[1:] + majorVersion := strings.Split(v, ".")[0] + if majorVersion != "3" { + return fmt.Errorf("this plugin requires helm V3 but got v%s", v) + } + return nil +} + +func NewHelmChartInflationGeneratorPlugin() resmap.GeneratorPlugin { + return &HelmChartInflationGeneratorPlugin{} +} diff --git a/api/builtins/PatchJson6902Transformer.go b/api/builtins/PatchJson6902Transformer.go index 7d56a15f3..2051c874a 100644 --- a/api/builtins/PatchJson6902Transformer.go +++ b/api/builtins/PatchJson6902Transformer.go @@ -10,7 +10,6 @@ import ( "github.com/pkg/errors" "sigs.k8s.io/kustomize/api/filters/patchjson6902" "sigs.k8s.io/kustomize/api/ifc" - "sigs.k8s.io/kustomize/api/resid" "sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kyaml/filtersutil" @@ -20,9 +19,9 @@ import ( type PatchJson6902TransformerPlugin struct { ldr ifc.Loader decodedPatch jsonpatch.Patch - Target types.PatchTarget `json:"target,omitempty" yaml:"target,omitempty"` - Path string `json:"path,omitempty" yaml:"path,omitempty"` - JsonOp string `json:"jsonOp,omitempty" yaml:"jsonOp,omitempty"` + Target *types.Selector `json:"target,omitempty" yaml:"target,omitempty"` + Path string `json:"path,omitempty" yaml:"path,omitempty"` + JsonOp string `json:"jsonOp,omitempty" yaml:"jsonOp,omitempty"` } func (p *PatchJson6902TransformerPlugin) Config( @@ -72,22 +71,22 @@ func (p *PatchJson6902TransformerPlugin) Config( } func (p *PatchJson6902TransformerPlugin) Transform(m resmap.ResMap) error { - id := resid.NewResIdWithNamespace( - resid.Gvk{ - Group: p.Target.Group, - Version: p.Target.Version, - Kind: p.Target.Kind, - }, - p.Target.Name, - p.Target.Namespace, - ) - obj, err := m.GetById(id) + if p.Target == nil { + return fmt.Errorf("must specify a target for patch %s", p.JsonOp) + } + resources, err := m.Select(*p.Target) if err != nil { return err } - return filtersutil.ApplyToJSON(patchjson6902.Filter{ - Patch: p.JsonOp, - }, obj) + for _, res := range resources { + err = filtersutil.ApplyToJSON(patchjson6902.Filter{ + Patch: p.JsonOp, + }, res) + if err != nil { + return err + } + } + return nil } func NewPatchJson6902TransformerPlugin() resmap.TransformerPlugin { diff --git a/api/filters/patchstrategicmerge/patchstrategicmerge_test.go b/api/filters/patchstrategicmerge/patchstrategicmerge_test.go index 867f83db6..81456e9bb 100644 --- a/api/filters/patchstrategicmerge/patchstrategicmerge_test.go +++ b/api/filters/patchstrategicmerge/patchstrategicmerge_test.go @@ -370,6 +370,213 @@ spec: image: test2 - name: test image: test +`, + }, + "list map keys - add a port, no names": { + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-deployment +spec: + template: + spec: + containers: + - image: test-image + name: test-deployment + ports: + - containerPort: 8080 + protocol: TCP +`, + patch: yaml.MustParse(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-deployment +spec: + template: + spec: + containers: + - image: test-image + name: test-deployment + ports: + - containerPort: 8080 + protocol: UDP + - containerPort: 80 + protocol: UDP +`), + expected: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-deployment +spec: + template: + spec: + containers: + - image: test-image + name: test-deployment + ports: + - containerPort: 8080 + protocol: UDP + - containerPort: 80 + protocol: UDP + - containerPort: 8080 + protocol: TCP +`, + }, + "list map keys - add name to port": { + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-deployment +spec: + template: + spec: + containers: + - image: test-image + name: test-deployment + ports: + - containerPort: 8080 + protocol: UDP + - containerPort: 8080 + protocol: TCP +`, + patch: yaml.MustParse(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-deployment +spec: + template: + spec: + containers: + - image: test-image + name: test-deployment + ports: + - containerPort: 8080 + protocol: UDP + name: UDP-name-patch +`), + expected: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-deployment +spec: + template: + spec: + containers: + - image: test-image + name: test-deployment + ports: + - containerPort: 8080 + protocol: UDP + name: UDP-name-patch + - containerPort: 8080 + protocol: TCP +`, + }, + "list map keys - replace port name": { + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-deployment +spec: + template: + spec: + containers: + - image: test-image + name: test-deployment + ports: + - containerPort: 8080 + protocol: UDP + name: UDP-name-original + - containerPort: 8080 + protocol: TCP + name: TCP-name-original +`, + patch: yaml.MustParse(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-deployment +spec: + template: + spec: + containers: + - image: test-image + name: test-deployment + ports: + - containerPort: 8080 + protocol: UDP + name: UDP-name-patch +`), + expected: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-deployment +spec: + template: + spec: + containers: + - image: test-image + name: test-deployment + ports: + - containerPort: 8080 + protocol: UDP + name: UDP-name-patch + - containerPort: 8080 + protocol: TCP + name: TCP-name-original +`, + }, + "list map keys - add a port, no protocol": { + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-deployment +spec: + template: + spec: + containers: + - image: test-image + name: test-deployment + ports: + - containerPort: 8080 +`, + patch: yaml.MustParse(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-deployment +spec: + template: + spec: + containers: + - image: test-image + name: test-deployment + ports: + - containerPort: 80 +`), + expected: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: test-deployment +spec: + template: + spec: + containers: + - image: test-image + name: test-deployment + ports: + - containerPort: 80 + - containerPort: 8080 `, }, } diff --git a/api/go.mod b/api/go.mod index 143d90992..c3cf13b4a 100644 --- a/api/go.mod +++ b/api/go.mod @@ -6,7 +6,9 @@ require ( github.com/evanphx/json-patch v4.5.0+incompatible github.com/go-openapi/spec v0.19.5 github.com/golangci/golangci-lint v1.21.0 + github.com/google/go-cmp v0.3.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/hashicorp/go-multierror v1.1.0 github.com/pkg/errors v0.8.1 github.com/stretchr/testify v1.4.0 github.com/yujunz/go-getter v1.4.1-lite @@ -17,8 +19,8 @@ require ( k8s.io/apimachinery v0.17.0 k8s.io/client-go v0.17.0 k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a - sigs.k8s.io/kustomize/kyaml v0.9.3 + sigs.k8s.io/kustomize/kyaml v0.9.4 sigs.k8s.io/yaml v1.2.0 ) -replace sigs.k8s.io/kustomize/kyaml v0.9.3 => ../kyaml +replace sigs.k8s.io/kustomize/kyaml v0.9.4 => ../kyaml diff --git a/api/go.sum b/api/go.sum index 7f0470b13..c2e3f304e 100644 --- a/api/go.sum +++ b/api/go.sum @@ -232,8 +232,12 @@ github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:Fecb github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6KdvN3Gig= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0= diff --git a/api/internal/crawl/go.sum b/api/internal/crawl/go.sum index e92a9abd0..67e46c4d9 100644 --- a/api/internal/crawl/go.sum +++ b/api/internal/crawl/go.sum @@ -206,8 +206,10 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:Fecb github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6KdvN3Gig= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0= @@ -527,8 +529,8 @@ k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw= -sigs.k8s.io/kustomize/kyaml v0.9.3 h1:kZ5HnNmmnbndSXFivrAVM6u3Ik1dwu4kbpaV8KNwy8I= -sigs.k8s.io/kustomize/kyaml v0.9.3/go.mod h1:UTm64bSWVdBUA8EQoYCxVOaBQxUdIOr5LKWxA4GNbkw= +sigs.k8s.io/kustomize/kyaml v0.9.4 h1:DDuzZtjIzFqp2IPy4DTyCI69Cl3bDgcJODjI6sjF9NY= +sigs.k8s.io/kustomize/kyaml v0.9.4/go.mod h1:UTm64bSWVdBUA8EQoYCxVOaBQxUdIOr5LKWxA4GNbkw= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/api/internal/plugins/builtinhelpers/builtinplugintype_string.go b/api/internal/plugins/builtinhelpers/builtinplugintype_string.go index 24d85a187..7b775fadb 100644 --- a/api/internal/plugins/builtinhelpers/builtinplugintype_string.go +++ b/api/internal/plugins/builtinhelpers/builtinplugintype_string.go @@ -23,11 +23,12 @@ func _() { _ = x[ReplicaCountTransformer-12] _ = x[SecretGenerator-13] _ = x[ValueAddTransformer-14] + _ = x[HelmChartInflationGenerator-15] } -const _BuiltinPluginType_name = "UnknownAnnotationsTransformerConfigMapGeneratorHashTransformerImageTagTransformerLabelTransformerLegacyOrderTransformerNamespaceTransformerPatchJson6902TransformerPatchStrategicMergeTransformerPatchTransformerPrefixSuffixTransformerReplicaCountTransformerSecretGeneratorValueAddTransformer" +const _BuiltinPluginType_name = "UnknownAnnotationsTransformerConfigMapGeneratorHashTransformerImageTagTransformerLabelTransformerLegacyOrderTransformerNamespaceTransformerPatchJson6902TransformerPatchStrategicMergeTransformerPatchTransformerPrefixSuffixTransformerReplicaCountTransformerSecretGeneratorValueAddTransformerHelmChartInflationGenerator" -var _BuiltinPluginType_index = [...]uint16{0, 7, 29, 47, 62, 81, 97, 119, 139, 163, 193, 209, 232, 255, 270, 289} +var _BuiltinPluginType_index = [...]uint16{0, 7, 29, 47, 62, 81, 97, 119, 139, 163, 193, 209, 232, 255, 270, 289, 316} func (i BuiltinPluginType) String() string { if i < 0 || i >= BuiltinPluginType(len(_BuiltinPluginType_index)-1) { diff --git a/api/internal/plugins/builtinhelpers/builtins.go b/api/internal/plugins/builtinhelpers/builtins.go index 60934fde8..f53583981 100644 --- a/api/internal/plugins/builtinhelpers/builtins.go +++ b/api/internal/plugins/builtinhelpers/builtins.go @@ -27,6 +27,7 @@ const ( ReplicaCountTransformer SecretGenerator ValueAddTransformer + HelmChartInflationGenerator ) var stringToBuiltinPluginTypeMap map[string]BuiltinPluginType @@ -55,8 +56,9 @@ func GetBuiltinPluginType(n string) BuiltinPluginType { } var GeneratorFactories = map[BuiltinPluginType]func() resmap.GeneratorPlugin{ - ConfigMapGenerator: builtins.NewConfigMapGeneratorPlugin, - SecretGenerator: builtins.NewSecretGeneratorPlugin, + ConfigMapGenerator: builtins.NewConfigMapGeneratorPlugin, + SecretGenerator: builtins.NewSecretGeneratorPlugin, + HelmChartInflationGenerator: builtins.NewHelmChartInflationGeneratorPlugin, } var TransformerFactories = map[BuiltinPluginType]func() resmap.TransformerPlugin{ diff --git a/api/internal/plugins/doc.go b/api/internal/plugins/doc.go index 83e428352..74d8ae378 100644 --- a/api/internal/plugins/doc.go +++ b/api/internal/plugins/doc.go @@ -94,7 +94,7 @@ TO GENERATE CODE cd $repo/plugin/builtin go generate ./... -See travis/kyaml-pre-commit.sh for canonical way +See scripts/kyaml-pre-commit.sh for canonical way to execute the above. This creates diff --git a/api/internal/target/kusttarget.go b/api/internal/target/kusttarget.go index d5d1626c9..0fe3c6daf 100644 --- a/api/internal/target/kusttarget.go +++ b/api/internal/target/kusttarget.go @@ -8,6 +8,7 @@ import ( "fmt" "strings" + "github.com/hashicorp/go-multierror" "github.com/pkg/errors" "sigs.k8s.io/kustomize/api/builtins" "sigs.k8s.io/kustomize/api/ifc" @@ -313,12 +314,18 @@ func (kt *KustTarget) accumulateResources( if errF := kt.accumulateFile(ra, path); errF != nil { ldr, errL := kt.ldr.New(path) if errL != nil { - return nil, fmt.Errorf("accumulateFile %q, loader.New %q", errF, errL) + return nil, multierror.Append( + fmt.Errorf("accumulateFile error: %q", errF), + fmt.Errorf("loader.New error: %q", errL), + ) } var errD error ra, errD = kt.accumulateDirectory(ra, ldr, false) if errD != nil { - return nil, fmt.Errorf("accumulateFile %q, accumulateDirector: %q", errF, errD) + return nil, multierror.Append( + fmt.Errorf("accumulateFile error: %q", errF), + fmt.Errorf("accumulateDirector error: %q", errD), + ) } } } diff --git a/api/internal/target/kusttarget_configplugin.go b/api/internal/target/kusttarget_configplugin.go index 324dc8999..a46b2e6ba 100644 --- a/api/internal/target/kusttarget_configplugin.go +++ b/api/internal/target/kusttarget_configplugin.go @@ -32,6 +32,7 @@ func (kt *KustTarget) configureBuiltinGenerators() ( for _, bpt := range []builtinhelpers.BuiltinPluginType{ builtinhelpers.ConfigMapGenerator, builtinhelpers.SecretGenerator, + builtinhelpers.HelmChartInflationGenerator, } { r, err := generatorConfigurators[bpt]( kt, bpt, builtinhelpers.GeneratorFactories[bpt]) @@ -110,6 +111,23 @@ var generatorConfigurators = map[builtinhelpers.BuiltinPluginType]func( } return }, + + builtinhelpers.HelmChartInflationGenerator: func(kt *KustTarget, bpt builtinhelpers.BuiltinPluginType, f gFactory) ( + result []resmap.Generator, err error) { + var c struct { + types.HelmChartArgs + } + for _, args := range kt.kustomization.HelmChartInflationGenerator { + c.HelmChartArgs = args + p := f() + err := kt.configureBuiltinPlugin(p, c, bpt) + if err != nil { + return nil, err + } + result = append(result, p) + } + return + }, } type tFactory func() resmap.TransformerPlugin @@ -141,12 +159,12 @@ var transformerConfigurators = map[builtinhelpers.BuiltinPluginType]func( kt *KustTarget, bpt builtinhelpers.BuiltinPluginType, f tFactory, _ *builtinconfig.TransformerConfig) ( result []resmap.Transformer, err error) { var c struct { - Target types.PatchTarget `json:"target,omitempty" yaml:"target,omitempty"` - Path string `json:"path,omitempty" yaml:"path,omitempty"` - JsonOp string `json:"jsonOp,omitempty" yaml:"jsonOp,omitempty"` + Target *types.Selector `json:"target,omitempty" yaml:"target,omitempty"` + Path string `json:"path,omitempty" yaml:"path,omitempty"` + JsonOp string `json:"jsonOp,omitempty" yaml:"jsonOp,omitempty"` } for _, args := range kt.kustomization.PatchesJson6902 { - c.Target = *args.Target + c.Target = args.Target c.Path = args.Path c.JsonOp = args.Patch p := f() diff --git a/api/internal/wrappy/factory_test.go b/api/internal/wrappy/factory_test.go index 7db433cb6..4eea8268b 100644 --- a/api/internal/wrappy/factory_test.go +++ b/api/internal/wrappy/factory_test.go @@ -1,4 +1,4 @@ // Copyright 2020 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 -package wrappy_test +package wrappy diff --git a/api/internal/wrappy/wnode.go b/api/internal/wrappy/wnode.go index be3bb8485..0989dbc09 100644 --- a/api/internal/wrappy/wnode.go +++ b/api/internal/wrappy/wnode.go @@ -4,7 +4,9 @@ package wrappy import ( + "fmt" "log" + "strings" "sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/resid" @@ -54,10 +56,41 @@ func (wn *WNode) GetAnnotations() map[string]string { // GetFieldValue implements ifc.Kunstructured. func (wn *WNode) GetFieldValue(path string) (interface{}, error) { - // The argument is a json path, e.g. "metadata.name" - // fields := strings.Split(path, ".") - // return wn.node.Pipe(yaml.Lookup(fields...)) - panic("TODO(#WNode): GetFieldValue; implement or drop from API") + fields := strings.Split(path, ".") + rn, err := wn.node.Pipe(yaml.Lookup(fields...)) + if err != nil { + return nil, err + } + if rn == nil { + return nil, NoFieldError{path} + } + yn := rn.YNode() + + // If this is an alias node, resolve it + if yn.Kind == yaml.AliasNode { + yn = yn.Alias + } + + // Return value as map for DocumentNode and MappingNode kinds + if yn.Kind == yaml.DocumentNode || yn.Kind == yaml.MappingNode { + var result map[string]interface{} + if err := yn.Decode(&result); err != nil { + return nil, err + } + return result, err + } + + // Return value as slice for SequenceNode kind + if yn.Kind == yaml.SequenceNode { + var result []interface{} + for _, node := range yn.Content { + result = append(result, node.Value) + } + return result, nil + } + + // Return value value directly for all other (ScalarNode) kinds + return yn.Value, nil } // GetGvk implements ifc.Kunstructured. @@ -83,18 +116,37 @@ func (wn *WNode) GetName() string { } // GetSlice implements ifc.Kunstructured. -func (wn *WNode) GetSlice(string) ([]interface{}, error) { - panic("TODO(#WNode) GetSlice; implement or drop from API") +func (wn *WNode) GetSlice(path string) ([]interface{}, error) { + value, err := wn.GetFieldValue(path) + if err != nil { + return nil, err + } + if sliceValue, ok := value.([]interface{}); ok { + return sliceValue, nil + } + return nil, fmt.Errorf("node %s is not a slice", path) } // GetSlice implements ifc.Kunstructured. -func (wn *WNode) GetString(string) (string, error) { - panic("TODO(#WNode) GetString; implement or drop from API") +func (wn *WNode) GetString(path string) (string, error) { + value, err := wn.GetFieldValue(path) + if err != nil { + return "", err + } + if v, ok := value.(string); ok { + return v, nil + } + return "", fmt.Errorf("node %s is not a string: %v", path, value) } // Map implements ifc.Kunstructured. func (wn *WNode) Map() map[string]interface{} { - panic("TODO(#WNode) Map; implement or drop from API") + var result map[string]interface{} + if err := wn.node.YNode().Decode(&result); err != nil { + // Log and die since interface doesn't allow error. + log.Fatalf("failed to decode ynode: %v", err) + } + return result } // MarshalJSON implements ifc.Kunstructured. @@ -113,31 +165,51 @@ func (wn *WNode) MatchesLabelSelector(string) (bool, error) { } // SetAnnotations implements ifc.Kunstructured. -func (wn *WNode) SetAnnotations(map[string]string) { - panic("TODO(#WNode) SetAnnotations; implement or drop from API") +func (wn *WNode) SetAnnotations(annotations map[string]string) { + wn.setField(yaml.NewMapRNode(&annotations), yaml.MetadataField, yaml.AnnotationsField) } // SetGvk implements ifc.Kunstructured. -func (wn *WNode) SetGvk(resid.Gvk) { - panic("TODO(#WNode) SetGvk; implement or drop from API") +func (wn *WNode) SetGvk(gvk resid.Gvk) { + wn.setField(yaml.NewScalarRNode(gvk.Kind), yaml.KindField) + wn.setField(yaml.NewScalarRNode(fmt.Sprintf("%s/%s", gvk.Group, gvk.Version)), yaml.APIVersionField) } // SetLabels implements ifc.Kunstructured. -func (wn *WNode) SetLabels(map[string]string) { - panic("TODO(#WNode) SetLabels; implement or drop from API") +func (wn *WNode) SetLabels(labels map[string]string) { + wn.setField(yaml.NewMapRNode(&labels), yaml.MetadataField, yaml.LabelsField) } // SetName implements ifc.Kunstructured. -func (wn *WNode) SetName(string) { - panic("TODO(#WNode) SetName; implement or drop from API") +func (wn *WNode) SetName(name string) { + wn.setField(yaml.NewScalarRNode(name), yaml.MetadataField, yaml.NameField) } // SetNamespace implements ifc.Kunstructured. -func (wn *WNode) SetNamespace(string) { - panic("TODO(#WNode) SetNamespace; implement or drop from API") +func (wn *WNode) SetNamespace(ns string) { + wn.setField(yaml.NewScalarRNode(ns), yaml.MetadataField, yaml.NamespaceField) +} + +func (wn *WNode) setField(value *yaml.RNode, path ...string) { + err := wn.node.PipeE( + yaml.LookupCreate(yaml.MappingNode, path[0:len(path)-1]...), + yaml.SetField(path[len(path)-1], value), + ) + if err != nil { + // Log and die since interface doesn't allow error. + log.Fatalf("failed to set field %v: %v", path, err) + } } // UnmarshalJSON implements ifc.Kunstructured. func (wn *WNode) UnmarshalJSON(data []byte) error { return wn.node.UnmarshalJSON(data) } + +type NoFieldError struct { + Field string +} + +func (e NoFieldError) Error() string { + return fmt.Sprintf("no field named '%s'", e.Field) +} diff --git a/api/internal/wrappy/wnode_test.go b/api/internal/wrappy/wnode_test.go index 072373a23..379c8b7d3 100644 --- a/api/internal/wrappy/wnode_test.go +++ b/api/internal/wrappy/wnode_test.go @@ -1,14 +1,16 @@ // Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 -package wrappy_test +package wrappy import ( "strings" "testing" + "github.com/google/go-cmp/cmp" + "sigs.k8s.io/kustomize/api/resid" + "gopkg.in/yaml.v3" - . "sigs.k8s.io/kustomize/api/internal/wrappy" kyaml "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -337,3 +339,215 @@ func TestGettingFields(t *testing.T) { t.Fatalf("unexpected annotations '%v'", actualMap) } } + +func TestGetFieldValueReturnsMap(t *testing.T) { + wn := NewWNode() + if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + expected := map[string]interface{}{ + "fruit": "apple", + "veggie": "carrot", + } + actual, err := wn.GetFieldValue("metadata.labels") + if err != nil { + t.Fatalf("error getting field value: %v", err) + } + if diff := cmp.Diff(expected, actual); diff != "" { + t.Fatalf("actual map does not deep equal expected map:\n%v", diff) + } +} + +func TestGetFieldValueReturnsSlice(t *testing.T) { + bytes, err := yaml.Marshal(makeBigMap()) + if err != nil { + t.Fatalf("unexpected yaml.Marshal err: %v", err) + } + rNode, err := kyaml.Parse(string(bytes)) + if err != nil { + t.Fatalf("unexpected yaml.Marshal err: %v", err) + } + wn := FromRNode(rNode) + expected := []interface{}{"idx0", "idx1", "idx2", "idx3"} + actual, err := wn.GetFieldValue("that") + if err != nil { + t.Fatalf("error getting slice: %v", err) + } + if diff := cmp.Diff(expected, actual); diff != "" { + t.Fatalf("actual slice does not deep equal expected slice:\n%v", diff) + } +} + +func TestGetFieldValueReturnsString(t *testing.T) { + wn := NewWNode() + if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + actual, err := wn.GetFieldValue("metadata.labels.fruit") + if err != nil { + t.Fatalf("error getting field value: %v", err) + } + v, ok := actual.(string) + if !ok || v != "apple" { + t.Fatalf("unexpected value '%v'", actual) + } +} + +func TestGetFieldValueResolvesAlias(t *testing.T) { + yamlWithAlias := ` +foo: &a theValue +bar: *a +` + rNode, err := kyaml.Parse(yamlWithAlias) + if err != nil { + t.Fatalf("unexpected yaml parse error: %v", err) + } + wn := FromRNode(rNode) + actual, err := wn.GetFieldValue("bar") + if err != nil { + t.Fatalf("error getting field value: %v", err) + } + v, ok := actual.(string) + if !ok || v != "theValue" { + t.Fatalf("unexpected value '%v'", actual) + } +} + +func TestGetString(t *testing.T) { + wn := NewWNode() + if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + expected := "carrot" + actual, err := wn.GetString("metadata.labels.veggie") + if err != nil { + t.Fatalf("error getting string: %v", err) + } + if expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } +} + +func TestGetSlice(t *testing.T) { + bytes, err := yaml.Marshal(makeBigMap()) + if err != nil { + t.Fatalf("unexpected yaml.Marshal err: %v", err) + } + rNode, err := kyaml.Parse(string(bytes)) + if err != nil { + t.Fatalf("unexpected yaml.Marshal err: %v", err) + } + wn := FromRNode(rNode) + expected := []interface{}{"idx0", "idx1", "idx2", "idx3"} + actual, err := wn.GetSlice("that") + if err != nil { + t.Fatalf("error getting slice: %v", err) + } + if diff := cmp.Diff(expected, actual); diff != "" { + t.Fatalf("actual slice does not deep equal expected slice:\n%v", diff) + } +} + +func TestMap(t *testing.T) { + wn := NewWNode() + if err := wn.UnmarshalJSON([]byte(deploymentLittleJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + + expected := map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "homer", + "namespace": "simpsons", + }, + } + + actual := wn.Map() + if diff := cmp.Diff(expected, actual); diff != "" { + t.Fatalf("actual map does not deep equal expected map:\n%v", diff) + } +} + +func TestSetName(t *testing.T) { + wn := NewWNode() + if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + wn.SetName("marge") + if expected, actual := "marge", wn.GetName(); expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } +} + +func TestSetNamespace(t *testing.T) { + wn := NewWNode() + if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + wn.SetNamespace("flanders") + meta, _ := wn.node.GetMeta() + if expected, actual := "flanders", meta.Namespace; expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } +} + +func TestSetLabels(t *testing.T) { + wn := NewWNode() + if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + wn.SetLabels(map[string]string{ + "label1": "foo", + "label2": "bar", + }) + labels := wn.GetLabels() + if expected, actual := 2, len(labels); expected != actual { + t.Fatalf("expected '%d', got '%d'", expected, actual) + } + if expected, actual := "foo", labels["label1"]; expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } + if expected, actual := "bar", labels["label2"]; expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } +} + +func TestGetAnnotations(t *testing.T) { + wn := NewWNode() + if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + wn.SetAnnotations(map[string]string{ + "annotation1": "foo", + "annotation2": "bar", + }) + annotations := wn.GetAnnotations() + if expected, actual := 2, len(annotations); expected != actual { + t.Fatalf("expected '%d', got '%d'", expected, actual) + } + if expected, actual := "foo", annotations["annotation1"]; expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } + if expected, actual := "bar", annotations["annotation2"]; expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } +} + +func TestSetGvk(t *testing.T) { + wn := NewWNode() + if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + wn.SetGvk(resid.GvkFromString("grp_ver_knd")) + gvk := wn.GetGvk() + if expected, actual := "grp", gvk.Group; expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } + if expected, actual := "ver", gvk.Version; expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } + if expected, actual := "knd", gvk.Kind; expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } +} diff --git a/api/krusty/helmchartinflationgenerator_test.go b/api/krusty/helmchartinflationgenerator_test.go new file mode 100644 index 000000000..7ed02f85d --- /dev/null +++ b/api/krusty/helmchartinflationgenerator_test.go @@ -0,0 +1,103 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package krusty_test + +/* +import ( + "testing" + + kusttest_test "sigs.k8s.io/kustomize/api/testutils/kusttest" +) + +var expected string = ` +apiVersion: v1 +data: + rcon-password: Q0hBTkdFTUUh +kind: Secret +metadata: + labels: + app: test-minecraft + chart: minecraft-1.2.0 + heritage: Helm + release: test + name: test-minecraft +type: Opaque +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + annotations: + volume.alpha.kubernetes.io/storage-class: default + labels: + app: test-minecraft + chart: minecraft-1.2.0 + heritage: Helm + release: test + name: test-minecraft-datadir +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: test-minecraft + chart: minecraft-1.2.0 + heritage: Helm + release: test + name: test-minecraft +spec: + ports: + - name: minecraft + port: 25565 + protocol: TCP + targetPort: minecraft + selector: + app: test-minecraft + type: LoadBalancer +` + +func TestHelmChartInflationGenerator(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK("/app", ` +helmChartInflationGenerator: +- chartName: minecraft + chartRepoUrl: https://kubernetes-charts.storage.googleapis.com + chartVersion: v1.2.0 + releaseName: test + releaseNamespace: testNamespace +`) + + m := th.Run("/app", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, expected) +} + + +func TestHelmChartInflationGeneratorAsPlugin(t *testing.T) { + th := kusttest_test.MakeHarness(t) + th.WriteK("/app", ` +generators: +- helm.yaml +`) + + th.WriteF("/app/helm.yaml", ` +apiVersion: builtin +kind: HelmChartInflationGenerator +metadata: + name: myMap +chartName: minecraft +chartRepoUrl: https://kubernetes-charts.storage.googleapis.com +chartVersion: v1.2.0 +releaseName: test +releaseNamespace: testNamespace +`) + + m := th.Run("/app", th.MakeDefaultOptions()) + th.AssertActualEqualsExpected(m, expected) +} +*/ diff --git a/api/resmap/reswrangler.go b/api/resmap/reswrangler.go index ce83767e6..f439831ba 100644 --- a/api/resmap/reswrangler.go +++ b/api/resmap/reswrangler.go @@ -6,7 +6,6 @@ package resmap import ( "bytes" "fmt" - "regexp" "github.com/pkg/errors" "sigs.k8s.io/kustomize/api/resid" @@ -510,51 +509,34 @@ func (m *resWrangler) appendReplaceOrMerge( return nil } -func anchorRegex(pattern string) string { - if pattern == "" { - return pattern - } - return "^(?:" + pattern + ")$" -} - // Select returns a list of resources that // are selected by a Selector func (m *resWrangler) Select(s types.Selector) ([]*resource.Resource, error) { - ns := regexp.MustCompile(anchorRegex(s.Namespace)) - nm := regexp.MustCompile(anchorRegex(s.Name)) var result []*resource.Resource + sr, err := types.NewSelectorRegex(&s) + if err != nil { + return nil, err + } for _, r := range m.Resources() { curId := r.CurId() orgId := r.OrgId() - // matches the namespace when namespace is not empty in the selector // It first tries to match with the original namespace // then matches with the current namespace - if s.Namespace != "" { - matched := ns.MatchString(orgId.EffectiveNamespace()) - if !matched { - matched = ns.MatchString(curId.EffectiveNamespace()) - if !matched { - continue - } - } + if !sr.MatchNamespace(orgId.EffectiveNamespace()) && + !sr.MatchNamespace(curId.EffectiveNamespace()) { + continue } - // matches the name when name is not empty in the selector // It first tries to match with the original name // then matches with the current name - if s.Name != "" { - matched := nm.MatchString(orgId.Name) - if !matched { - matched = nm.MatchString(curId.Name) - if !matched { - continue - } - } + if !sr.MatchName(orgId.Name) && + !sr.MatchName(curId.Name) { + continue } // matches the GVK - if !r.GetGvk().IsSelected(&s.Gvk) { + if !sr.MatchGvk(r.GetGvk()) { continue } diff --git a/api/types/helmchartargs.go b/api/types/helmchartargs.go new file mode 100644 index 000000000..96ebdb949 --- /dev/null +++ b/api/types/helmchartargs.go @@ -0,0 +1,19 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package types + +// HelmChartArgs contains the metadata of how to generate a secret. +type HelmChartArgs struct { + ChartName string `json:"chartName,omitempty" yaml:"chartName,omitempty"` + ChartVersion string `json:"chartVersion,omitempty" yaml:"chartVersion,omitempty"` + ChartRepoURL string `json:"chartRepoUrl,omitempty" yaml:"chartRepoUrl,omitempty"` + ChartHome string `json:"chartHome,omitempty" yaml:"chartHome,omitempty"` + // Use chartRelease to keep compatible with old exec plugin + ChartRepoName string `json:"chartRelease,omitempty" yaml:"chartRelease,omitempty"` + HelmBin string `json:"helmBin,omitempty" yaml:"helmBin,omitempty"` + HelmHome string `json:"helmHome,omitempty" yaml:"helmHome,omitempty"` + Values string `json:"values,omitempty" yaml:"values,omitempty"` + ReleaseName string `json:"releaseName,omitempty" yaml:"releaseName,omitempty"` + ReleaseNamespace string `json:"releaseNamespace,omitempty" yaml:"releaseNamespace,omitempty"` +} diff --git a/api/types/kustomization.go b/api/types/kustomization.go index 6be8053f4..b290da35c 100644 --- a/api/types/kustomization.go +++ b/api/types/kustomization.go @@ -55,7 +55,7 @@ type Kustomization struct { // JSONPatches is a list of JSONPatch for applying JSON patch. // Format documented at https://tools.ietf.org/html/rfc6902 // and http://jsonpatch.com - PatchesJson6902 []PatchJson6902 `json:"patchesJson6902,omitempty" yaml:"patchesJson6902,omitempty"` + PatchesJson6902 []Patch `json:"patchesJson6902,omitempty" yaml:"patchesJson6902,omitempty"` // Patches is a list of patches, where each one can be either a // Strategic Merge Patch or a JSON patch. @@ -122,6 +122,11 @@ type Kustomization struct { // the map will have a suffix hash generated from its contents. SecretGenerator []SecretArgs `json:"secretGenerator,omitempty" yaml:"secretGenerator,omitempty"` + // HelmChartInflationGenerator is a list of helm chart configurations. + // The resulting resource is a normal operand rendered from + // a remote chart by `helm template` + HelmChartInflationGenerator []HelmChartArgs `json:"helmChartInflationGenerator,omitempty" yaml:"helmChartInflationGenerator,omitempty"` + // GeneratorOptions modify behavior of all ConfigMap and Secret generators. GeneratorOptions *GeneratorOptions `json:"generatorOptions,omitempty" yaml:"generatorOptions,omitempty"` @@ -167,9 +172,7 @@ func (k *Kustomization) FixKustomizationPostUnmarshalling() { // has been processed. func (k *Kustomization) FixKustomizationPreMarshalling() { // PatchesJson6902 should be under the Patches field. - for _, patch := range k.PatchesJson6902 { - k.Patches = append(k.Patches, patch.ToPatch()) - } + k.Patches = append(k.Patches, k.PatchesJson6902...) k.PatchesJson6902 = nil } diff --git a/api/types/patchjson6902.go b/api/types/patchjson6902.go deleted file mode 100644 index 4c90ce201..000000000 --- a/api/types/patchjson6902.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package types - -// PatchJson6902 represents a json patch for an object -// with format documented https://tools.ietf.org/html/rfc6902. -type PatchJson6902 struct { - // PatchTarget refers to a Kubernetes object that the json patch will be - // applied to. It must refer to a Kubernetes resource under the - // purview of this kustomization. PatchTarget should use the - // raw name of the object (the name specified in its YAML, - // before addition of a namePrefix and a nameSuffix). - Target *PatchTarget `json:"target" yaml:"target"` - - // relative file path for a json patch file inside a kustomization - Path string `json:"path,omitempty" yaml:"path,omitempty"` - - // inline patch string - Patch string `json:"patch,omitempty" yaml:"patch,omitempty"` -} - -// ToPatch converts a PatchJson6902 to its superset Patch. -func (patch *PatchJson6902) ToPatch() Patch { - selector := patch.Target.ToSelector() - return Patch{Path: patch.Path, Patch: patch.Patch, Target: &selector} -} diff --git a/api/types/patchtarget.go b/api/types/patchtarget.go deleted file mode 100644 index 85f804574..000000000 --- a/api/types/patchtarget.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package types - -import ( - "sigs.k8s.io/kustomize/api/resid" -) - -// PatchTarget represents the kubernetes object that the patch is applied to -type PatchTarget struct { - resid.Gvk `json:",inline,omitempty" yaml:",inline,omitempty"` - Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` - Name string `json:"name" yaml:"name"` -} - -// ToSelector converts a PatchTarget to a Selector. -func (target *PatchTarget) ToSelector() Selector { - return Selector{Name: target.Name, Namespace: target.Namespace, Gvk: target.Gvk} -} diff --git a/api/types/selector.go b/api/types/selector.go index c58c5a985..007b508f1 100644 --- a/api/types/selector.go +++ b/api/types/selector.go @@ -4,6 +4,8 @@ package types import ( + "regexp" + "sigs.k8s.io/kustomize/api/resid" ) @@ -25,3 +27,89 @@ type Selector struct { // It matches with the resource labels. LabelSelector string `json:"labelSelector,omitempty" yaml:"labelSelector,omitempty"` } + +// SelectorRegex is a Selector with regex in GVK +// Any resource that matches intersection of all conditions +// is included in this set. +type SelectorRegex struct { + selector *Selector + groupRegex *regexp.Regexp + versionRegex *regexp.Regexp + kindRegex *regexp.Regexp + nameRegex *regexp.Regexp + namespaceRegex *regexp.Regexp +} + +// NewSelectorRegex returns a pointer to a new SelectorRegex +// which uses the same condition as s. +func NewSelectorRegex(s *Selector) (*SelectorRegex, error) { + sr := new(SelectorRegex) + var err error + sr.selector = s + sr.groupRegex, err = regexp.Compile(anchorRegex(s.Gvk.Group)) + if err != nil { + return nil, err + } + sr.versionRegex, err = regexp.Compile(anchorRegex(s.Gvk.Version)) + if err != nil { + return nil, err + } + sr.kindRegex, err = regexp.Compile(anchorRegex(s.Gvk.Kind)) + if err != nil { + return nil, err + } + sr.nameRegex, err = regexp.Compile(anchorRegex(s.Name)) + if err != nil { + return nil, err + } + sr.namespaceRegex, err = regexp.Compile(anchorRegex(s.Namespace)) + if err != nil { + return nil, err + } + return sr, nil +} + +func anchorRegex(pattern string) string { + if pattern == "" { + return pattern + } + return "^(?:" + pattern + ")$" +} + +// MatchGvk return true if gvk can be matched by s. +func (s *SelectorRegex) MatchGvk(gvk resid.Gvk) bool { + if len(s.selector.Gvk.Group) > 0 { + if !s.groupRegex.MatchString(gvk.Group) { + return false + } + } + if len(s.selector.Gvk.Version) > 0 { + if !s.versionRegex.MatchString(gvk.Version) { + return false + } + } + if len(s.selector.Gvk.Kind) > 0 { + if !s.kindRegex.MatchString(gvk.Kind) { + return false + } + } + return true +} + +// MatchName returns true if the name in selector is +// empty or the n can be matches by the name in selector +func (s *SelectorRegex) MatchName(n string) bool { + if s.selector.Name == "" { + return true + } + return s.nameRegex.MatchString(n) +} + +// MatchNamespace returns true if the namespace in selector is +// empty or the ns can be matches by the namespace in selector +func (s *SelectorRegex) MatchNamespace(ns string) bool { + if s.selector.Namespace == "" { + return true + } + return s.namespaceRegex.MatchString(ns) +} diff --git a/api/types/selector_test.go b/api/types/selector_test.go new file mode 100644 index 000000000..9625d9964 --- /dev/null +++ b/api/types/selector_test.go @@ -0,0 +1,216 @@ +package types_test + +import ( + "testing" + + "sigs.k8s.io/kustomize/api/resid" + . "sigs.k8s.io/kustomize/api/types" +) + +func TestSelectorRegexMatchGvk(t *testing.T) { + testcases := []struct { + S Selector + G resid.Gvk + Expected bool + }{ + { + S: Selector{ + Gvk: resid.Gvk{ + Group: "group", + Version: "version", + Kind: "kind", + }, + }, + G: resid.Gvk{ + Group: "group", + Version: "version", + Kind: "kind", + }, + Expected: true, + }, + { + S: Selector{ + Gvk: resid.Gvk{ + Group: "group", + Version: "", + Kind: "", + }, + }, + G: resid.Gvk{ + Group: "group", + Version: "version", + Kind: "kind", + }, + Expected: true, + }, + { + S: Selector{ + Gvk: resid.Gvk{ + Group: "group", + Version: "version", + Kind: "kind", + }, + }, + G: resid.Gvk{ + Group: "group", + Version: "version", + Kind: "", + }, + Expected: false, + }, + { + S: Selector{ + Gvk: resid.Gvk{ + Group: "group", + Version: "version", + Kind: "kind", + }, + }, + G: resid.Gvk{ + Group: "group", + Version: "version", + Kind: "kind2", + }, + Expected: false, + }, + { + S: Selector{ + Gvk: resid.Gvk{ + Group: "g.*", + Version: "\\d+", + Kind: ".{4}", + }, + }, + G: resid.Gvk{ + Group: "group", + Version: "123", + Kind: "abcd", + }, + Expected: true, + }, + { + S: Selector{ + Gvk: resid.Gvk{ + Group: "g.*", + Version: "\\d+", + Kind: ".{4}", + }, + }, + G: resid.Gvk{ + Group: "group", + Version: "123", + Kind: "abc", + }, + Expected: false, + }, + } + + for _, tc := range testcases { + sr, err := NewSelectorRegex(&tc.S) + if err != nil { + t.Fatal(err) + } + if sr.MatchGvk(tc.G) != tc.Expected { + t.Fatalf("unexpected result for selector gvk %s and gvk %s", + tc.S.Gvk.String(), tc.G.String()) + } + } +} + +func TestSelectorRegexMatchName(t *testing.T) { + testcases := []struct { + S Selector + Name string + Expected bool + }{ + { + S: Selector{ + Name: "foo", + Namespace: "bar", + }, + Name: "foo", + Expected: true, + }, + { + S: Selector{ + Name: "foo", + Namespace: "bar", + }, + Name: "bar", + Expected: false, + }, + { + S: Selector{ + Name: "f.*", + }, + Name: "foo", + Expected: true, + }, + { + S: Selector{ + Name: "b.*", + }, + Name: "foo", + Expected: false, + }, + } + for _, tc := range testcases { + sr, err := NewSelectorRegex(&tc.S) + if err != nil { + t.Fatal(err) + } + if sr.MatchName(tc.Name) != tc.Expected { + t.Fatalf("unexpected result for selector name %s and name %s", + tc.S.Name, tc.Name) + } + } +} + +func TestSelectorRegexMatchNamespace(t *testing.T) { + testcases := []struct { + S Selector + Namespace string + Expected bool + }{ + { + S: Selector{ + Name: "bar", + Namespace: "foo", + }, + Namespace: "foo", + Expected: true, + }, + { + S: Selector{ + Name: "foo", + Namespace: "bar", + }, + Namespace: "foo", + Expected: false, + }, + { + S: Selector{ + Namespace: "f.*", + }, + Namespace: "foo", + Expected: true, + }, + { + S: Selector{ + Namespace: "b.*", + }, + Namespace: "foo", + Expected: false, + }, + } + for _, tc := range testcases { + sr, err := NewSelectorRegex(&tc.S) + if err != nil { + t.Fatal(err) + } + if sr.MatchNamespace(tc.Namespace) != tc.Expected { + t.Fatalf("unexpected result for selector namespace %s and namespace %s", + tc.S.Namespace, tc.Namespace) + } + } +} diff --git a/cmd/config/go.mod b/cmd/config/go.mod index 95bd201b5..fb3b278e8 100644 --- a/cmd/config/go.mod +++ b/cmd/config/go.mod @@ -14,8 +14,8 @@ require ( golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect - k8s.io/apimachinery v0.17.3 - sigs.k8s.io/kustomize/kyaml v0.9.3 + k8s.io/apimachinery v0.18.10 + sigs.k8s.io/kustomize/kyaml v0.9.4 ) -replace sigs.k8s.io/kustomize/kyaml v0.9.3 => ../../kyaml +replace sigs.k8s.io/kustomize/kyaml v0.9.4 => ../../kyaml diff --git a/cmd/config/go.sum b/cmd/config/go.sum index 12984bd0f..b226a15db 100644 --- a/cmd/config/go.sum +++ b/cmd/config/go.sum @@ -34,7 +34,6 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -45,10 +44,9 @@ github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/dustmop/soup v1.1.2-0.20190516214245-38228baa104e/go.mod h1:CgNC6SGbT+Xb8wGGvzilttZL1mc5sQ/5KkcxsZttMIk= -github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M= -github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -122,8 +120,8 @@ github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= @@ -141,13 +139,15 @@ github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= @@ -161,7 +161,7 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -196,7 +196,6 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -213,8 +212,7 @@ github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -222,7 +220,7 @@ github.com/paulmach/orb v0.1.3/go.mod h1:VFlX/8C+IQ1p6FTRRKzKoOPJnvEtA5G0Veuqwbu github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -259,7 +257,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -330,9 +327,9 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c h1:Vco5b+cuG5NNfORVxZy6bYZQ7rsigisU1WQFkvQ0L5E= golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -383,14 +380,15 @@ gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/apimachinery v0.17.3 h1:f+uZV6rm4/tHE7xXgLyToprg6xWairaClGVkm2t8omg= -k8s.io/apimachinery v0.17.3/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g= +k8s.io/apimachinery v0.18.10 h1:Zupk3lPrUfhCF9puTpA8EvEfPsrhNZtrpOqdp66mKVs= +k8s.io/apimachinery v0.18.10/go.mod h1:PF5taHbXgTEJLU+xMypMmYTXTWPJ5LaW8bfsisxnEXk= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= -k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= -k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= -sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/cmd/config/runner/runner.go b/cmd/config/runner/runner.go index eb08b7be9..3d58d368a 100644 --- a/cmd/config/runner/runner.go +++ b/cmd/config/runner/runner.go @@ -60,37 +60,34 @@ func (e ExecuteCmdOnPkgs) Execute() error { } for i := range pkgsPaths { - pkgPath := pkgsPaths[i] - // Add schema present in openAPI file for current package - if e.NeedOpenAPI { - if err := openapi.AddSchemaFromFile(filepath.Join(pkgPath, ext.KRMFileName())); err != nil { - return err - } - } - - if !e.SkipPkgPathPrint { - fmt.Fprintf(e.Writer, "%s/\n", pkgPath) - } - - err := e.CmdRunner.ExecuteCmd(e.Writer, pkgPath) + err := e.processPkg(pkgsPaths[i]) if err != nil { return err } - if i != len(pkgsPaths)-1 { fmt.Fprint(e.Writer, "\n") } - - // Delete schema present in openAPI file for current package - if e.NeedOpenAPI { - if err := openapi.DeleteSchemaInFile(filepath.Join(pkgPath, ext.KRMFileName())); err != nil { - return err - } - } } return nil } +func (e ExecuteCmdOnPkgs) processPkg(pkgPath string) error { + // Add schema present in openAPI file for current package + if e.NeedOpenAPI { + clean, err := openapi.AddSchemaFromFile(filepath.Join(pkgPath, ext.KRMFileName())) + if err != nil { + return err + } + defer clean() + } + + if !e.SkipPkgPathPrint { + fmt.Fprintf(e.Writer, "%s/\n", pkgPath) + } + + return e.CmdRunner.ExecuteCmd(e.Writer, pkgPath) +} + // ParseFieldPath parse a flag value into a field path func ParseFieldPath(path string) ([]string, error) { // fixup '\.' so we don't split on it diff --git a/cmd/pluginator/go.mod b/cmd/pluginator/go.mod index a5b6f027a..9149e6043 100644 --- a/cmd/pluginator/go.mod +++ b/cmd/pluginator/go.mod @@ -2,6 +2,6 @@ module sigs.k8s.io/kustomize/cmd/pluginator/v2 go 1.14 -require sigs.k8s.io/kustomize/api v0.6.4 +require sigs.k8s.io/kustomize/api v0.6.5 -replace sigs.k8s.io/kustomize/api v0.6.4 => ../../api +replace sigs.k8s.io/kustomize/api v0.6.5 => ../../api diff --git a/cmd/pluginator/go.sum b/cmd/pluginator/go.sum index 2fab51b94..e3cd7a391 100644 --- a/cmd/pluginator/go.sum +++ b/cmd/pluginator/go.sum @@ -190,7 +190,9 @@ github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:Fecb github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -482,8 +484,8 @@ k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= mvdan.cc/unparam v0.0.0-20190720180237-d51796306d8f/go.mod h1:4G1h5nDURzA3bwVMZIVpwbkw+04kSxk3rAtzlimaUJw= -sigs.k8s.io/kustomize/kyaml v0.9.3 h1:kZ5HnNmmnbndSXFivrAVM6u3Ik1dwu4kbpaV8KNwy8I= -sigs.k8s.io/kustomize/kyaml v0.9.3/go.mod h1:UTm64bSWVdBUA8EQoYCxVOaBQxUdIOr5LKWxA4GNbkw= +sigs.k8s.io/kustomize/kyaml v0.9.4 h1:DDuzZtjIzFqp2IPy4DTyCI69Cl3bDgcJODjI6sjF9NY= +sigs.k8s.io/kustomize/kyaml v0.9.4/go.mod h1:UTm64bSWVdBUA8EQoYCxVOaBQxUdIOr5LKWxA4GNbkw= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/cmd/prchecker/main.go b/cmd/prchecker/main.go index 369b764bd..4459e0d61 100644 --- a/cmd/prchecker/main.go +++ b/cmd/prchecker/main.go @@ -19,11 +19,77 @@ import ( "flag" "fmt" "os" + "regexp" "strings" "github.com/google/go-github/github" ) +// splitCommitsHelp is a help message displayed when a PR has commits which +// span modules. +const splitCommitsHelp = "\nCommits that include multiple Go modules must be split into multiple commits that each touch no more than one module.\n" + + "Splitting instructions: https://git-scm.com/docs/git-rebase#_splitting_commits\n" + +// Ignore span pattern defines a regular expression pattern that, when matched, +// causes this check to be ignored. +// Pattern expects "ALLOW_MODULE_SPAN" in CAPS on a line by itself. +// Spaces may be included before or after but no other characters with one +// exception. ">" may be used to quote the exclusion. +// Pattern may be provided at any point in the pull request description. +// +// Ex: "ALLOW_MODULE_SPAN", "> ALLOW_MODULE_SPAN", " ALLOW_MODULE_SPAN " +const ignoreSpanPattern = "(?m)^>?\\s*ALLOW_MODULE_SPAN\\s*$" + +// Changeset represents a set of file modifications associated with a commit +type Changeset struct { + files []string + id string +} + +// GitHubRepository represents a pairing of the owner and repository to make +// passing around references to specific projects simpler +type GitHubRepository struct { + client *github.Client + owner *string + repo *string +} + +// GitHubService is the collection of GitHub API interactions this service consumes +type GitHubService interface { + GetPullRequest(prId int) (*github.PullRequest, *github.Response, error) + GetCommit(commitSha string) (*github.RepositoryCommit, *github.Response, error) + ListCommits(prId int, options *github.ListOptions) ([]*github.RepositoryCommit, *github.Response, error) +} + +// GetPullRequest retrieves details about a pull request from GitHub API +func (repository GitHubRepository) GetPullRequest(prId int) ( + *github.PullRequest, *github.Response, error) { + return repository.client.PullRequests.Get( + context.Background(), + *repository.owner, + *repository.repo, + prId) +} + +// GetCommit retrieves commit details from GitHub API +func (repository GitHubRepository) GetCommit(commitSha string) ( + *github.RepositoryCommit, *github.Response, error) { + return repository.client.Repositories.GetCommit(context.Background(), + *repository.owner, + *repository.repo, + commitSha) +} + +// ListCommits lists commits in a PR via GitHub API +func (repository GitHubRepository) ListCommits(prId int, options *github.ListOptions) ( + []*github.RepositoryCommit, *github.Response, error) { + return repository.client.PullRequests.ListCommits(context.Background(), + *repository.owner, + *repository.repo, + prId, + options) +} + func main() { owner := flag.String("owner", "", "the github repository owner name") repo := flag.String("repo", "", "the github repository name") @@ -41,79 +107,165 @@ func main() { client := github.NewClient(nil) - files, err := ListAllPullRequestFiles(client, owner, pullrequest, repo) + githubRepo := &GitHubRepository{ + client: client, + owner: owner, + repo: repo, + } + + // Check if module span is allowed before scanning on commits + isSpanAllowed, err := ModuleSpanAllowed(githubRepo, *pullrequest) if err != nil { - fmt.Println("Unable to retrieve pull request details:", err.Error()) + fmt.Printf("unable to retrieve pull request details: %v", err.Error()) os.Exit(2) } - contributedRestrictedPaths := CountRestrictedPathUses(files, restrictedPaths) - modifiedRestrictedDirectories := CountModifiedRestrictedDirectories(contributedRestrictedPaths) + if isSpanAllowed { + fmt.Println("Check not run. Module spanning was allowed in this Pull Request.") + os.Exit(0) + } + + isSpanningPull, _, err := PullRequestSpanningPathList(githubRepo, *pullrequest, restrictedPaths) + + if err != nil { + fmt.Printf("unable to retrieve pull request details: %v", err) + os.Exit(2) + } // Exit with error if two or more restricted directories where modified - if modifiedRestrictedDirectories > 1 { - fmt.Println("Modifications to multiple restricted directories occurred.") + if isSpanningPull { + // Provide a suggestion for potential solution if the check fails. + fmt.Println(splitCommitsHelp) os.Exit(1) } } -// ListAllPullRequestFiles retrieves as many files as possible for the -// target pull request. -// -// Note: GitHub API limits ListFiles to a maximum of 3000 files. Very large -// changes which exceed this limit may pass this check even if they -// do contain spanning changes. -// see: https://developer.github.com/v3/pulls/#list-pull-requests-files -func ListAllPullRequestFiles(client *github.Client, owner *string, pullrequest *int, repo *string) ([]*github.CommitFile, error) { +// ConstructChangeset creates a changeset from a GitHub Commit object +func ConstructChangeset(commit *github.RepositoryCommit) *Changeset { + id := commit.SHA + fileset := []string{} + + for _, file := range commit.Files { + fileset = append(fileset, *file.Filename) + } + + return &Changeset{ + files: fileset, + id: *id, + } +} + +// StringAllowsModuleSpan tests if a string matches the allow span regex +func StringAllowsModuleSpan(body string) (bool, error) { + return regexp.MatchString(ignoreSpanPattern, body) +} + +// ModuleSpanAllowed tests a Pull Requests description for a regular +// expression. If the expression matches then spanning modules are allowed. +func ModuleSpanAllowed(repository GitHubService, pullId int) (bool, error) { + + // Note: There are multiple ways to pull a github commit object + // we want a RepositoryCommit. + pullRequest, _, err := repository.GetPullRequest(pullId) + + if err != nil { + return false, err + } + + return StringAllowsModuleSpan(*pullRequest.Body) +} + +// GetCommitChanges looks up a github commit by SHA and returns a Changeset +// containing the modified files in the specified commit. +func GetCommitChanges(repository GitHubService, commitSha string) (*Changeset, error) { + commit, _, err := repository.GetCommit(commitSha) + + if err != nil { + return nil, err + } + + return ConstructChangeset(commit), nil +} + +// GetPullRequestCommits constructs a list of all commits and the associated +// files changes in the given pull request +func GetPullRequestCommits(repository GitHubService, pullrequest int) ([]*Changeset, error) { + // foundFiles across all pages from github api - var foundFiles []*github.CommitFile - // GitHub returns the first 30 files by default, increase this value - // Note: Page 1 is the first page of results. Page 0 is an end of list mark. - // Github only returns (max) 100 results per page and PR's may exceed this - // so loop until all pages have been enumerated. - options := &github.ListOptions{PerPage: 100, Page: 1} + var collectedCommits []*github.RepositoryCommit + // Github only returns a limited set of commits per request and PR's may + // exceed this so loop until all pages have been enumerated. + options := &github.ListOptions{Page: 1} for options.Page != 0 { - files, response, err := client.PullRequests.ListFiles(context.Background(), *owner, *repo, *pullrequest, options) + commits, response, err := repository.ListCommits(pullrequest, options) // If an error has occurred while querying api exit early, report error if err != nil { return nil, err } - foundFiles = append(foundFiles, files...) + collectedCommits = append(collectedCommits, commits...) // setup next page to continue loop - options = &github.ListOptions{PerPage: 100, Page: response.NextPage} + options = &github.ListOptions{Page: response.NextPage} } - return foundFiles, nil + + var changesetResults []*Changeset + for _, commit := range collectedCommits { + // The repository commits from list commits are not hydrated + // We will need to retrieve the complete object: + changeset, err := GetCommitChanges(repository, *commit.SHA) + if err != nil { + return nil, err + } + + changesetResults = append(changesetResults, changeset) + } + return changesetResults, nil } -// CountModifiedRestrictedDirectories Accepts a map of paths and the number of -// occurances and returns the count of the paths which had a non-zero value. -func CountModifiedRestrictedDirectories(contributedRestrictedPaths map[string]int) int { - modifiedRestrictedDirectories := 0 - for _, occurance := range contributedRestrictedPaths { - if occurance != 0 { - modifiedRestrictedDirectories++ +// PullRequestSpanningPathList tests if a pull request spans multiple +// directory paths +func PullRequestSpanningPathList(repository GitHubService, pullrequest int, paths []string) (bool, []*Changeset, error) { + // Create a buffer for commits + changesets, err := GetPullRequestCommits(repository, pullrequest) + + if err != nil { + return false, nil, err + } + + spanningChangesExist := false + for _, changeset := range changesets { + if changeset.isSpanningPaths(paths) { + // When detecting the first spanning changeset print a prefix message + if !spanningChangesExist { + fmt.Printf("Spanning changesets detected in the following commits:\n\n") + } + + fmt.Printf("\t* %s\n", changeset.id) + // In order provide a full list of outstanding commits, do not shortcircuit this check + spanningChangesExist = true } } - return modifiedRestrictedDirectories + + return spanningChangesExist, changesets, nil } -// CountRestrictedPathUses Constructs a map that contains the number of -// references keyed to each restricted path. This provides details about how -// many files in the list are associated with each restricted path. -func CountRestrictedPathUses(files []*github.CommitFile, restrictedPaths []string) map[string]int { - contributedRestrictedPaths := make(map[string]int) - for _, path := range restrictedPaths { - contributedRestrictedPaths[path] = 0 - } +// isSpanningPaths tests if a changeset is spanning +// multiple directory paths. +func (changeset *Changeset) isSpanningPaths(paths []string) bool { + matchedPath := "" - for _, file := range files { - for path := range contributedRestrictedPaths { - if strings.HasPrefix(*file.Filename, path) { - contributedRestrictedPaths[path]++ + for _, file := range changeset.files { + for _, path := range paths { + if strings.HasPrefix(file, path) { + // If a different path has already matched then the changeset spans multiple restricted paths + if matchedPath != "" && matchedPath != path { + return true + } + matchedPath = path } } } - return contributedRestrictedPaths + + return false } diff --git a/cmd/prchecker/main_test.go b/cmd/prchecker/main_test.go new file mode 100644 index 000000000..88bea9c31 --- /dev/null +++ b/cmd/prchecker/main_test.go @@ -0,0 +1,303 @@ +package main + +import ( + "testing" + + "github.com/google/go-github/github" +) + +func TestStringAllowsModuleSpan(t *testing.T) { + var tests = []struct { + name string + body string + matchExpected bool + }{ + { + "exception not included", + "foo", + false, + }, + { + "exception mentioned in sentence does not exempt span check", + "don't ALLOW_MODULE_SPAN", + false, + }, + { + "PR body is just exception", + "ALLOW_MODULE_SPAN", + true, + }, + { + "support markdown quoting exception", + "> ALLOW_MODULE_SPAN", + true, + }, + { + "support whitespace padding", + "\t ALLOW_MODULE_SPAN\t ", + true, + }, + { + "module span exemption allowed at start of string", + "ALLOW_MODULE_SPAN\nat start of file", + true, + }, + { + "module span exemption allowed at end of string", + "at end of file\nALLOW_MODULE_SPAN", + true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := StringAllowsModuleSpan(tt.body) + + if err != nil { + t.Errorf(err.Error()) + } + + if result != tt.matchExpected { + t.Errorf("got %t, want %t", result, tt.matchExpected) + } + }) + } +} + +func TestIsModuleSpanAllowed(t *testing.T) { + var tests = []struct { + name string + body string + matchExpected bool + }{ + { + "module spanning not allowed", + "don't ALLOW_MODULE_SPAN", + false, + }, + { + "module spanning allowed", + "ALLOW_MODULE_SPAN", + true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + client := FakeGitHubService{ + pullRequest: &github.PullRequest{ + Body: &tt.body, + }, + } + + result, _ := ModuleSpanAllowed(&client, 1) + + if result != tt.matchExpected { + t.Errorf("got %t, want %t", result, tt.matchExpected) + } + }) + } +} + +func TestGetCommitChanges(t *testing.T) { + client := FakeGitHubService{ + commit: &github.RepositoryCommit{ + SHA: github.String("abc123"), + Files: []github.CommitFile{{Filename: github.String("foo")}, {Filename: github.String("bar")}}, + }, + } + + result, _ := GetCommitChanges(&client, "abc123") + + if result.id != "abc123" { + t.Errorf("got %v, want %v", result.id, "abc123") + } + + if len(result.files) != 2 { + t.Errorf("got %v, want %v", len(result.files), "2") + } + + expectedFiles := []string{"foo", "bar"} + if !StringSliceAreEqual(result.files, expectedFiles) { + t.Errorf("got %v, want %v", result.files, expectedFiles) + } +} + +func TestGetPullRequestCommits(t *testing.T) { + client := FakeGitHubService{ + commit: &github.RepositoryCommit{ + SHA: github.String("abc123"), + Files: []github.CommitFile{{Filename: github.String("foo")}, {Filename: github.String("bar")}}, + }, + commitList: []*github.RepositoryCommit{ + { + SHA: github.String("abc123"), + }, + { + SHA: github.String("abc123"), + }, + }, + } + + result, _ := GetPullRequestCommits(&client, 42) + + if len(result) != 2 { + t.Errorf("got %v, want %v", len(result), 2) + } + + expectedFiles := []string{"foo", "bar"} + if !StringSliceAreEqual(result[0].files, expectedFiles) { + t.Errorf("[%d] got %v, want %v", 0, result[0].files, expectedFiles) + } + if !StringSliceAreEqual(result[1].files, expectedFiles) { + t.Errorf("[%d] got %v, want %v", 1, result[1].files, expectedFiles) + } +} + +func TestContstructingChangeset(t *testing.T) { + var tests = []struct { + name string + sha string + files []github.CommitFile + expected Changeset + }{ + { + "construct from single file", + "abc123", + []github.CommitFile{{Filename: github.String("foo")}}, + Changeset{id: "abc123", files: []string{"foo"}}, + }, + { + "construct from multiple files", + "1234", + []github.CommitFile{{Filename: github.String("foo")}, {Filename: github.String("bar")}}, + Changeset{id: "1234", files: []string{"foo", "bar"}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + commit := &github.RepositoryCommit{SHA: github.String(tt.sha), Files: tt.files} + result := ConstructChangeset(commit) + + if tt.expected.id != result.id { + t.Errorf("got %v, want %v", result.id, tt.expected.id) + } + + if !StringSliceAreEqual(tt.expected.files, result.files) { + t.Errorf("got %v, want %v", result.files, tt.expected.files) + } + }) + } +} + +func TestIsChangesetSpanning(t *testing.T) { + var tests = []struct { + name string + changeset []string + files []string + expected bool + }{ + { + "matching sets of 1 element do not span", + []string{"1"}, + []string{"1"}, + false, + }, + { + "subdirectories do not match top level directories", + []string{"a/1"}, + []string{"1"}, + false, + }, + { + "distinct sets do not span", + []string{"1", "2"}, + []string{"a", "b"}, + false, + }, + { + "single matching path does not span", + []string{"1", "a"}, + []string{"1", "2"}, + false, + }, + { + "path and subdirectory of same restriction do not span", + []string{"1", "1/a"}, + []string{"1", "2", "3"}, + false, + }, + { + "matching sets span", + []string{"1", "2"}, + []string{"1", "2"}, + true, + }, + { + "superset of restricted paths spans", + []string{"1", "2", "3"}, + []string{"1", "2"}, + true, + }, + { + "subset of restricted paths spans", + []string{"1", "3"}, + []string{"1", "2", "3"}, + true, + }, + { + "subdirectories of restricted paths span", + []string{"1/a", "3/b"}, + []string{"1", "2", "3"}, + true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + changeset := &Changeset{files: tt.changeset} + + result := changeset.isSpanningPaths(tt.files) + + if result != tt.expected { + t.Errorf("got %t, want %t", result, tt.expected) + } + }) + } +} + +type FakeGitHubService struct { + pullRequest *github.PullRequest + commit *github.RepositoryCommit + commitList []*github.RepositoryCommit +} + +func (repository FakeGitHubService) GetPullRequest(prId int) (*github.PullRequest, *github.Response, error) { + return repository.pullRequest, nil, nil +} + +func (repository FakeGitHubService) GetCommit(commitSha string) (*github.RepositoryCommit, *github.Response, error) { + return repository.commit, nil, nil + +} + +func (repository FakeGitHubService) ListCommits(prId int, options *github.ListOptions) ([]*github.RepositoryCommit, *github.Response, error) { + return repository.commitList, &github.Response{NextPage: 0}, nil +} + +func StringSliceAreEqual(left []string, right []string) bool { + if len(left) != len(right) { + return false + } + + for i, elem := range left { + if elem != right[i] { + return false + } + } + + return true +} diff --git a/docs/api-reference/glossary/index.html b/docs/api-reference/glossary/index.html index 20b312b8a..463591d1c 100644 --- a/docs/api-reference/glossary/index.html +++ b/docs/api-reference/glossary/index.html @@ -3,7 +3,7 @@
- + @@ -29,7 +29,8 @@ +"> + @@ -38,8 +39,8 @@ - - + + + + + + + +