From 04a2c57d548ee71f2206aa094c9729c600fca1e7 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Thu, 18 Oct 2018 10:44:35 +0200 Subject: [PATCH 01/23] Initial plumbing --- cmd/controller/main.go | 32 ++++++++--- pkg/admission/admission.go | 17 ++++++ pkg/admission/machinedeployments.go | 83 +++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 pkg/admission/admission.go create mode 100644 pkg/admission/machinedeployments.go diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 965f5655d..ba5c1e0a4 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -48,6 +48,7 @@ import ( "github.com/golang/glog" "github.com/heptiolabs/healthcheck" + "github.com/kubermatic/machine-controller/pkg/admission" "github.com/kubermatic/machine-controller/pkg/apis/cluster/v1alpha1/migrations" "github.com/kubermatic/machine-controller/pkg/clusterinfo" machinecontroller "github.com/kubermatic/machine-controller/pkg/controller/machine" @@ -66,12 +67,13 @@ import ( ) var ( - masterURL string - kubeconfig string - clusterDNSIPs string - listenAddress string - name string - workerCount int + masterURL string + kubeconfig string + clusterDNSIPs string + listenAddress string + admissionListenAddress string + name string + workerCount int ) const ( @@ -148,6 +150,7 @@ func main() { flag.IntVar(&workerCount, "worker-count", 5, "Number of workers to process machines. Using a high number with a lot of machines might cause getting rate-limited from your cloud provider.") flag.StringVar(&listenAddress, "internal-listen-address", "127.0.0.1:8085", "The address on which the http server will listen on. The server exposes metrics on /metrics, liveness check on /live and readiness check on /ready") flag.StringVar(&name, "name", "", "When set, the controller will only process machines with the label \"machine.k8s.io/controller\": name") + flag.StringVar(&admissionListenAddress, "admission-listen-address", ":9876", "The address on which the MutatingWebhook will listen on") flag.Parse() @@ -260,11 +263,24 @@ func main() { g.Add(func() error { return s.ListenAndServe() }, func(err error) { - glog.Warningf("shutting down HTTP server due to: %s", err) + glog.Warningf("shutting down util HTTP server due to: %s", err) srvCtx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() if err = s.Shutdown(srvCtx); err != nil { - glog.Errorf("failed to shutdown HTTP server: %s", err) + glog.Errorf("failed to shutdown util HTTP server: %s", err) + } + }) + } + { + s := admission.New(admissionListenAddress) + g.Add(func() error { + return s.ListenAndServe() + }, func(err error) { + glog.Warningf("shutting down admission HTTP server due to: %s", err) + srvCtx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + if err = s.Shutdown(srvCtx); err != nil { + glog.Errorf("failed to shutdown admission HTTP server: %s", err) } }) } diff --git a/pkg/admission/admission.go b/pkg/admission/admission.go new file mode 100644 index 000000000..08ce63751 --- /dev/null +++ b/pkg/admission/admission.go @@ -0,0 +1,17 @@ +package admission + +import ( + "net/http" + "time" +) + +func New(listenAddress string) *http.Server { + m := http.NewServeMux() + m.HandleFunc("/machinedeployments", handleFuncFactory(mutateMachineDeployments)) + return &http.Server{ + Addr: listenAddress, + Handler: m, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + } +} diff --git a/pkg/admission/machinedeployments.go b/pkg/admission/machinedeployments.go new file mode 100644 index 000000000..a09b974bc --- /dev/null +++ b/pkg/admission/machinedeployments.go @@ -0,0 +1,83 @@ +package admission + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/golang/glog" + + admissionv1beta1 "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + + clusterv1alpha1 "sigs.k8s.io/cluster-api/pkg/apis/cluster/v1alpha1" +) + +var codecs = serializer.NewCodecFactory(runtime.NewScheme()) + +func mutateMachineDeployments(ar admissionv1beta1.AdmissionReview) (*admissionv1beta1.AdmissionResponse, error) { + + machineDeployment := &clusterv1alpha1.MachineDeployment{} + if err := json.Unmarshal(ar.Request.Object.Raw, machineDeployment); err != nil { + return nil, fmt.Errorf("failed to unmarshal: %v", err) + } + + return nil, fmt.Errorf("unimplemented") +} + +func handleFuncFactory(mutate func(admissionv1beta1.AdmissionReview) (*admissionv1beta1.AdmissionResponse, error)) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var body []byte + if r.Body != nil { + if data, err := ioutil.ReadAll(r.Body); err == nil { + body = data + } + } + + // verify the content type is accurate + contentType := r.Header.Get("Content-Type") + if contentType != "application/json" { + glog.Errorf("contentType=%s, expect application/json", contentType) + return + } + + var reviewResponse *admissionv1beta1.AdmissionResponse + ar := admissionv1beta1.AdmissionReview{} + deserializer := codecs.UniversalDeserializer() + if _, _, err := deserializer.Decode(body, nil, &ar); err != nil { + glog.Error(err) + reviewResponse.Result = &metav1.Status{Message: err.Error()} + } else { + reviewResponse, err = mutate(ar) + if err != nil { + glog.Errorf("Error mutating %v", err) + } + } + + response := admissionv1beta1.AdmissionReview{} + if reviewResponse != nil { + response.Response = reviewResponse + response.Response.UID = ar.Request.UID + } else { + // Required to not have the apiserver crash with an NPE on older versions + // https://github.com/kubernetes/apiserver/commit/584fe98b6432033007b686f1b8063e05d20d328d + response.Response = &admissionv1beta1.AdmissionResponse{} + } + + // reset the Object and OldObject, they are not needed in a response. + ar.Request.Object = runtime.RawExtension{} + ar.Request.OldObject = runtime.RawExtension{} + + resp, err := json.Marshal(response) + if err != nil { + glog.Errorf("failed to marshal response: %v", err) + return + } + if _, err := w.Write(resp); err != nil { + glog.Errorf("failed to write response: %v", err) + } + } +} From d79d9fdcb8a42461e1a3e6b4f5e440fb6c1e6376 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Thu, 18 Oct 2018 12:21:22 +0200 Subject: [PATCH 02/23] Full plumbing --- .gitignore | 3 ++ Makefile | 28 ++++++++++++++++ cmd/controller/main.go | 6 +++- examples/machine-controller.yaml | 56 ++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e16f95f27..d1c0eb92c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ test/tools/verify/verify terraform terraform-provider-hcloud .kubeconfig +examples/*.pem +examples/*.csr +examples/*.srl diff --git a/Makefile b/Makefile index 97aa6ab16..5d79d3c14 100644 --- a/Makefile +++ b/Makefile @@ -68,3 +68,31 @@ e2e-cluster: e2e-destroy: ./test/tools/integration/cleanup_machines.sh make -C test/tools/integration destroy + +examples/ca-key.pem: + openssl genrsa -out examples/ca-key.pem 4096 + +examples/ca-cert.pem: examples/ca-key.pem + openssl req -x509 -new -nodes -key examples/ca-key.pem \ + -subj "/C=US/ST=CA/O=Acme/CN=k8s-machine-controller-ca" \ + -sha256 -days 10000 -out examples/ca-cert.pem + +examples/admission-key.pem: examples/ca-cert.pem + openssl genrsa -out examples/admission-key.pem 2048 + chmod 0600 examples/admission-key.pem + +examples/admission-cert.pem: examples/admission-key.pem + openssl req -new -sha256 \ + -key examples/admission-key.pem \ + -subj "/C=US/ST=CA/O=Acme/CN=machine-controller.kube-system.svc" \ + -out examples/admission.csr + openssl x509 -req -in examples/admission.csr -CA examples/ca-cert.pem \ + -CAkey examples/ca-key.pem -CAcreateserial \ + -out examples/admission-cert.pem -days 10000 -sha256 + +deploy: examples/admission-cert.pem + @cat examples/machine-controller.yaml \ + |sed "s/__admission_ca_cert__/$(shell cat examples/ca-cert.pem|base64 -w0)/g" \ + |sed "s/__admission_cert__/$(shell cat examples/admission-cert.pem|base64 -w0)/g" \ + |sed "s/__admission_key__/$(shell cat examples/admission-key.pem|base64 -w0)/g" \ + |kubectl apply -f - diff --git a/cmd/controller/main.go b/cmd/controller/main.go index ba5c1e0a4..641696e87 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -72,6 +72,8 @@ var ( clusterDNSIPs string listenAddress string admissionListenAddress string + admissionTLSCertPath string + admissionTLSKeyPath string name string workerCount int ) @@ -151,6 +153,8 @@ func main() { flag.StringVar(&listenAddress, "internal-listen-address", "127.0.0.1:8085", "The address on which the http server will listen on. The server exposes metrics on /metrics, liveness check on /live and readiness check on /ready") flag.StringVar(&name, "name", "", "When set, the controller will only process machines with the label \"machine.k8s.io/controller\": name") flag.StringVar(&admissionListenAddress, "admission-listen-address", ":9876", "The address on which the MutatingWebhook will listen on") + flag.StringVar(&admissionTLSCertPath, "admission-tls-cert-path", "/tmp/cert/cert.pem", "The path of the TLS cert for the MutatingWebhook") + flag.StringVar(&admissionTLSKeyPath, "admission-tls-key-path", "/tmp/cert/key.pem", "The path of the TLS key for the MutatingWebhook") flag.Parse() @@ -274,7 +278,7 @@ func main() { { s := admission.New(admissionListenAddress) g.Add(func() error { - return s.ListenAndServe() + return s.ListenAndServeTLS(admissionTLSCertPath, admissionTLSKeyPath) }, func(err error) { glog.Warningf("shutting down admission HTTP server due to: %s", err) srvCtx, cancel := context.WithTimeout(ctx, time.Second) diff --git a/examples/machine-controller.yaml b/examples/machine-controller.yaml index edb7c51e8..ecdffd873 100644 --- a/examples/machine-controller.yaml +++ b/examples/machine-controller.yaml @@ -175,6 +175,7 @@ spec: - -worker-count=5 - -cluster-dns=10.10.10.10 - -internal-listen-address=0.0.0.0:8085 + - -admission-listen-address=0.0.0.0:9876 ports: - containerPort: 8085 livenessProbe: @@ -188,6 +189,37 @@ spec: path: /ready port: 8085 periodSeconds: 5 + volumeMounts: + - name: machine-controller-admission-cert + mountPath: /tmp/cert + volumes: + - name: machine-controller-admission-cert + secret: + secretName: machine-controller-admission-cert +--- +apiVersion: v1 +kind: Secret +metadata: + name: machine-controller-admission-cert + namespace: kube-system +data: + "cert.pem": __admission_cert__ + "key.pem": __admission_key__ +--- +apiVersion: v1 +kind: Service +metadata: + name: machine-controller + namespace: kube-system +spec: + ports: + - name: 443-9876 + port: 443 + protocol: TCP + targetPort: 9876 + selector: + app: machine-controller + type: ClusterIP --- apiVersion: v1 kind: ServiceAccount @@ -404,3 +436,27 @@ subjects: - kind: ServiceAccount name: machine-controller namespace: kube-system +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: MutatingWebhookConfiguration +metadata: + name: machinedeployments.machine-controller.kubermatic.io +webhooks: +- name: machinedeployments.machine-controller.kubermatic.io + failurePolicy: Fail + rules: + - apiGroups: + - "cluster.k8s.io" + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - machinedeployments + clientConfig: + service: + namespace: kube-system + name: machine-controller + path: /machinedeployments + caBundle: __admission_ca_cert__ From 4fdf14384a9cbd4c92d4d62317d234e0126a29e4 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Thu, 18 Oct 2018 14:52:22 +0200 Subject: [PATCH 03/23] Add business logic to machineDeployment defaulting and validation --- Gopkg.lock | 14 + pkg/admission/admission.go | 24 ++ pkg/admission/machinedeployments.go | 34 ++- .../machinedeployments_validation.go | 132 +++++++++ vendor/github.com/mattbaird/jsonpatch/LICENSE | 202 ++++++++++++++ .../mattbaird/jsonpatch/jsonpatch.go | 257 ++++++++++++++++++ 6 files changed, 658 insertions(+), 5 deletions(-) create mode 100644 pkg/admission/machinedeployments_validation.go create mode 100644 vendor/github.com/mattbaird/jsonpatch/LICENSE create mode 100644 vendor/github.com/mattbaird/jsonpatch/jsonpatch.go diff --git a/Gopkg.lock b/Gopkg.lock index 2e4ae2ab6..0c553abfb 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -552,6 +552,14 @@ pruneopts = "NUT" revision = "03f2033d19d5860aef995fe360ac7d395cd8ce65" +[[projects]] + branch = "master" + digest = "1:0e9bfc47ab9941ecc3344e580baca5deb4091177e84dd9773b48b38ec26b93d5" + name = "github.com/mattbaird/jsonpatch" + packages = ["."] + pruneopts = "NUT" + revision = "81af80346b1a01caae0cbc27fd3c1ba5b11e189f" + [[projects]] digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6" name = "github.com/matttproud/golang_protobuf_extensions" @@ -1395,6 +1403,7 @@ "github.com/gophercloud/gophercloud/pagination", "github.com/heptiolabs/healthcheck", "github.com/hetznercloud/hcloud-go/hcloud", + "github.com/mattbaird/jsonpatch", "github.com/oklog/run", "github.com/pborman/uuid", "github.com/pmezard/go-difflib/difflib", @@ -1411,6 +1420,7 @@ "golang.org/x/oauth2", "gopkg.in/gcfg.v1", "gopkg.in/ini.v1", + "k8s.io/api/admission/v1beta1", "k8s.io/api/core/v1", "k8s.io/api/policy/v1beta1", "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1", @@ -1418,6 +1428,7 @@ "k8s.io/apimachinery/pkg/api/equality", "k8s.io/apimachinery/pkg/api/errors", "k8s.io/apimachinery/pkg/apis/meta/v1", + "k8s.io/apimachinery/pkg/apis/meta/v1/validation", "k8s.io/apimachinery/pkg/fields", "k8s.io/apimachinery/pkg/labels", "k8s.io/apimachinery/pkg/runtime", @@ -1426,10 +1437,13 @@ "k8s.io/apimachinery/pkg/selection", "k8s.io/apimachinery/pkg/types", "k8s.io/apimachinery/pkg/util/errors", + "k8s.io/apimachinery/pkg/util/intstr", "k8s.io/apimachinery/pkg/util/rand", "k8s.io/apimachinery/pkg/util/runtime", "k8s.io/apimachinery/pkg/util/sets", "k8s.io/apimachinery/pkg/util/uuid", + "k8s.io/apimachinery/pkg/util/validation", + "k8s.io/apimachinery/pkg/util/validation/field", "k8s.io/apimachinery/pkg/util/wait", "k8s.io/apimachinery/pkg/util/yaml", "k8s.io/apimachinery/pkg/watch", diff --git a/pkg/admission/admission.go b/pkg/admission/admission.go index 08ce63751..825affa90 100644 --- a/pkg/admission/admission.go +++ b/pkg/admission/admission.go @@ -1,8 +1,15 @@ package admission import ( + "encoding/json" + "fmt" "net/http" + "reflect" "time" + + "github.com/mattbaird/jsonpatch" + + "k8s.io/apimachinery/pkg/runtime" ) func New(listenAddress string) *http.Server { @@ -15,3 +22,20 @@ func New(listenAddress string) *http.Server { WriteTimeout: 10 * time.Second, } } + +func newJSONPatch(original, current runtime.Object) ([]jsonpatch.JsonPatchOperation, error) { + originalGVK := original.GetObjectKind().GroupVersionKind() + currentGVK := current.GetObjectKind().GroupVersionKind() + if !reflect.DeepEqual(originalGVK, currentGVK) { + return nil, fmt.Errorf("GroupVersionKind %#v is expected to match %#v", originalGVK, currentGVK) + } + ori, err := json.Marshal(original) + if err != nil { + return nil, err + } + cur, err := json.Marshal(current) + if err != nil { + return nil, err + } + return jsonpatch.CreatePatch(ori, cur) +} diff --git a/pkg/admission/machinedeployments.go b/pkg/admission/machinedeployments.go index a09b974bc..d21ed384b 100644 --- a/pkg/admission/machinedeployments.go +++ b/pkg/admission/machinedeployments.go @@ -16,16 +16,39 @@ import ( clusterv1alpha1 "sigs.k8s.io/cluster-api/pkg/apis/cluster/v1alpha1" ) -var codecs = serializer.NewCodecFactory(runtime.NewScheme()) +var ( + codecs = serializer.NewCodecFactory(runtime.NewScheme()) + jsonPatch = admissionv1beta1.PatchTypeJSONPatch +) func mutateMachineDeployments(ar admissionv1beta1.AdmissionReview) (*admissionv1beta1.AdmissionResponse, error) { - machineDeployment := &clusterv1alpha1.MachineDeployment{} - if err := json.Unmarshal(ar.Request.Object.Raw, machineDeployment); err != nil { + machineDeployment := clusterv1alpha1.MachineDeployment{} + if err := json.Unmarshal(ar.Request.Object.Raw, &machineDeployment); err != nil { return nil, fmt.Errorf("failed to unmarshal: %v", err) } + machineDeploymentOriginal := machineDeployment.DeepCopy() + + machineDeploymentDefaultingFunction(&machineDeployment) + if errs := validateMachineDeployment(machineDeployment); errs != nil { + return nil, fmt.Errorf("validation failed: %v", errs) + } + + patchOpts, err := newJSONPatch(machineDeploymentOriginal, &machineDeployment) + if err != nil { + return nil, fmt.Errorf("failed to create json patch: %v", err) + } + + patchRaw, err := json.Marshal(patchOpts) + if err != nil { + return nil, fmt.Errorf("failed to marshal json patch: %v", err) + } - return nil, fmt.Errorf("unimplemented") + response := &admissionv1beta1.AdmissionResponse{} + response.Allowed = true + response.Patch = patchRaw + response.PatchType = &jsonPatch + return response, nil } func handleFuncFactory(mutate func(admissionv1beta1.AdmissionReview) (*admissionv1beta1.AdmissionResponse, error)) func(http.ResponseWriter, *http.Request) { @@ -53,8 +76,9 @@ func handleFuncFactory(mutate func(admissionv1beta1.AdmissionReview) (*admission } else { reviewResponse, err = mutate(ar) if err != nil { - glog.Errorf("Error mutating %v", err) + glog.Errorf("Error mutating: %v", err) } + reviewResponse.Result = &metav1.Status{Message: fmt.Sprintf("Error mutating: %v", err)} } response := admissionv1beta1.AdmissionReview{} diff --git a/pkg/admission/machinedeployments_validation.go b/pkg/admission/machinedeployments_validation.go new file mode 100644 index 000000000..1c2cf5732 --- /dev/null +++ b/pkg/admission/machinedeployments_validation.go @@ -0,0 +1,132 @@ +package admission + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/intstr" + utilvalidation "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/apimachinery/pkg/util/validation/field" + "log" + "sigs.k8s.io/cluster-api/pkg/apis/cluster/common" + "sigs.k8s.io/cluster-api/pkg/apis/cluster/v1alpha1" +) + +func validateMachineDeployment(md v1alpha1.MachineDeployment) field.ErrorList { + log.Printf("Validating fields for MachineDeployment %s\n", md.Name) + allErrs := field.ErrorList{} + allErrs = append(allErrs, validateMachineDeploymentSpec(&md.Spec, field.NewPath("spec"))...) + return allErrs +} + +func validateMachineDeploymentSpec(spec *v1alpha1.MachineDeploymentSpec, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, metav1validation.ValidateLabelSelector(&spec.Selector, fldPath.Child("selector"))...) + if len(spec.Selector.MatchLabels)+len(spec.Selector.MatchExpressions) == 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), spec.Selector, "empty selector is not valid for MachineSet.")) + } + selector, err := metav1.LabelSelectorAsSelector(&spec.Selector) + if err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), spec.Selector, "invalid label selector.")) + } else { + labels := labels.Set(spec.Template.Labels) + if !selector.Matches(labels) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("template", "metadata", "labels"), spec.Template.Labels, "`selector` does not match template `labels`")) + } + } + if spec.Replicas == nil || *spec.Replicas < 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("replicas"), *spec.Replicas, "replicas must be specified and can not be negative")) + } + allErrs = append(allErrs, validateMachineDeploymentStrategy(&spec.Strategy, fldPath.Child("strategy"))...) + return allErrs +} + +func validateMachineDeploymentStrategy(strategy *v1alpha1.MachineDeploymentStrategy, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + switch strategy.Type { + case common.RollingUpdateMachineDeploymentStrategyType: + if strategy.RollingUpdate != nil { + allErrs = append(allErrs, validateMachineRollingUpdateDeployment(strategy.RollingUpdate, fldPath.Child("rollingUpdate"))...) + } + default: + allErrs = append(allErrs, field.Invalid(fldPath.Child("Type"), strategy.Type, "is an invalid type")) + } + return allErrs +} + +func validateMachineRollingUpdateDeployment(rollingUpdate *v1alpha1.MachineRollingUpdateDeployment, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + var maxUnavailable int + var maxSurge int + if rollingUpdate.MaxUnavailable != nil { + allErrs = append(allErrs, validatePositiveIntOrPercent(rollingUpdate.MaxUnavailable, fldPath.Child("maxUnavailable"))...) + maxUnavailable, _ = getIntOrPercent(rollingUpdate.MaxUnavailable, false) + // Validate that MaxUnavailable is not more than 100%. + if len(utilvalidation.IsValidPercent(rollingUpdate.MaxUnavailable.StrVal)) == 0 && maxUnavailable > 100 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("maxUnavailable"), rollingUpdate.MaxUnavailable, "should not be more than 100%")) + } + } + if rollingUpdate.MaxSurge != nil { + allErrs = append(allErrs, validatePositiveIntOrPercent(rollingUpdate.MaxSurge, fldPath.Child("maxSurge"))...) + maxSurge, _ = getIntOrPercent(rollingUpdate.MaxSurge, true) + } + if rollingUpdate.MaxUnavailable != nil && rollingUpdate.MaxSurge != nil && maxUnavailable == 0 && maxSurge == 0 { + // Both MaxSurge and MaxUnavailable cannot be zero. + allErrs = append(allErrs, field.Invalid(fldPath.Child("maxUnavailable"), rollingUpdate.MaxUnavailable, "may not be 0 when `maxSurge` is 0")) + } + return allErrs +} + +func validatePositiveIntOrPercent(s *intstr.IntOrString, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if x, err := getIntOrPercent(s, false); err != nil { + allErrs = append(allErrs, field.Invalid(fldPath, s.StrVal, "value should be int(5) or percentage(5%)")) + } else if x < 0 { + allErrs = append(allErrs, field.Invalid(fldPath, x, "value should not be negative")) + } + return allErrs +} +func getIntOrPercent(s *intstr.IntOrString, roundUp bool) (int, error) { + return intstr.GetValueFromIntOrPercent(s, 100, roundUp) +} + +func machineDeploymentDefaultingFunction(obj *v1alpha1.MachineDeployment) { + // set default field values here + log.Printf("Defaulting fields for MachineDeployment %s\n", obj.Name) + if obj.Spec.Replicas == nil { + obj.Spec.Replicas = new(int32) + *obj.Spec.Replicas = 1 + } + if obj.Spec.MinReadySeconds == nil { + obj.Spec.MinReadySeconds = new(int32) + *obj.Spec.MinReadySeconds = 0 + } + if obj.Spec.RevisionHistoryLimit == nil { + obj.Spec.RevisionHistoryLimit = new(int32) + *obj.Spec.RevisionHistoryLimit = 1 + } + if obj.Spec.ProgressDeadlineSeconds == nil { + obj.Spec.ProgressDeadlineSeconds = new(int32) + *obj.Spec.ProgressDeadlineSeconds = 600 + } + if obj.Spec.Strategy.Type == "" { + obj.Spec.Strategy.Type = common.RollingUpdateMachineDeploymentStrategyType + } + // Default RollingUpdate strategy only if strategy type is RollingUpdate. + if obj.Spec.Strategy.Type == common.RollingUpdateMachineDeploymentStrategyType { + if obj.Spec.Strategy.RollingUpdate == nil { + obj.Spec.Strategy.RollingUpdate = &v1alpha1.MachineRollingUpdateDeployment{} + } + if obj.Spec.Strategy.RollingUpdate.MaxSurge == nil { + x := intstr.FromInt(1) + obj.Spec.Strategy.RollingUpdate.MaxSurge = &x + } + if obj.Spec.Strategy.RollingUpdate.MaxUnavailable == nil { + x := intstr.FromInt(0) + obj.Spec.Strategy.RollingUpdate.MaxUnavailable = &x + } + } + if len(obj.Namespace) == 0 { + obj.Namespace = metav1.NamespaceDefault + } +} diff --git a/vendor/github.com/mattbaird/jsonpatch/LICENSE b/vendor/github.com/mattbaird/jsonpatch/LICENSE new file mode 100644 index 000000000..8f71f43fe --- /dev/null +++ b/vendor/github.com/mattbaird/jsonpatch/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/vendor/github.com/mattbaird/jsonpatch/jsonpatch.go b/vendor/github.com/mattbaird/jsonpatch/jsonpatch.go new file mode 100644 index 000000000..295f260f5 --- /dev/null +++ b/vendor/github.com/mattbaird/jsonpatch/jsonpatch.go @@ -0,0 +1,257 @@ +package jsonpatch + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" +) + +var errBadJSONDoc = fmt.Errorf("Invalid JSON Document") + +type JsonPatchOperation struct { + Operation string `json:"op"` + Path string `json:"path"` + Value interface{} `json:"value,omitempty"` +} + +func (j *JsonPatchOperation) Json() string { + b, _ := json.Marshal(j) + return string(b) +} + +func (j *JsonPatchOperation) MarshalJSON() ([]byte, error) { + var b bytes.Buffer + b.WriteString("{") + b.WriteString(fmt.Sprintf(`"op":"%s"`, j.Operation)) + b.WriteString(fmt.Sprintf(`,"path":"%s"`, j.Path)) + // Consider omitting Value for non-nullable operations. + if j.Value != nil || j.Operation == "replace" || j.Operation == "add" { + v, err := json.Marshal(j.Value) + if err != nil { + return nil, err + } + b.WriteString(`,"value":`) + b.Write(v) + } + b.WriteString("}") + return b.Bytes(), nil +} + +type ByPath []JsonPatchOperation + +func (a ByPath) Len() int { return len(a) } +func (a ByPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByPath) Less(i, j int) bool { return a[i].Path < a[j].Path } + +func NewPatch(operation, path string, value interface{}) JsonPatchOperation { + return JsonPatchOperation{Operation: operation, Path: path, Value: value} +} + +// CreatePatch creates a patch as specified in http://jsonpatch.com/ +// +// 'a' is original, 'b' is the modified document. Both are to be given as json encoded content. +// The function will return an array of JsonPatchOperations +// +// An error will be returned if any of the two documents are invalid. +func CreatePatch(a, b []byte) ([]JsonPatchOperation, error) { + aI := map[string]interface{}{} + bI := map[string]interface{}{} + err := json.Unmarshal(a, &aI) + if err != nil { + return nil, errBadJSONDoc + } + err = json.Unmarshal(b, &bI) + if err != nil { + return nil, errBadJSONDoc + } + return diff(aI, bI, "", []JsonPatchOperation{}) +} + +// Returns true if the values matches (must be json types) +// The types of the values must match, otherwise it will always return false +// If two map[string]interface{} are given, all elements must match. +func matchesValue(av, bv interface{}) bool { + if reflect.TypeOf(av) != reflect.TypeOf(bv) { + return false + } + switch at := av.(type) { + case string: + bt := bv.(string) + if bt == at { + return true + } + case float64: + bt := bv.(float64) + if bt == at { + return true + } + case bool: + bt := bv.(bool) + if bt == at { + return true + } + case map[string]interface{}: + bt := bv.(map[string]interface{}) + for key := range at { + if !matchesValue(at[key], bt[key]) { + return false + } + } + for key := range bt { + if !matchesValue(at[key], bt[key]) { + return false + } + } + return true + case []interface{}: + bt := bv.([]interface{}) + if len(bt) != len(at) { + return false + } + for key := range at { + if !matchesValue(at[key], bt[key]) { + return false + } + } + for key := range bt { + if !matchesValue(at[key], bt[key]) { + return false + } + } + return true + } + return false +} + +// From http://tools.ietf.org/html/rfc6901#section-4 : +// +// Evaluation of each reference token begins by decoding any escaped +// character sequence. This is performed by first transforming any +// occurrence of the sequence '~1' to '/', and then transforming any +// occurrence of the sequence '~0' to '~'. +// TODO decode support: +// var rfc6901Decoder = strings.NewReplacer("~1", "/", "~0", "~") + +var rfc6901Encoder = strings.NewReplacer("~", "~0", "/", "~1") + +func makePath(path string, newPart interface{}) string { + key := rfc6901Encoder.Replace(fmt.Sprintf("%v", newPart)) + if path == "" { + return "/" + key + } + if strings.HasSuffix(path, "/") { + return path + key + } + return path + "/" + key +} + +// diff returns the (recursive) difference between a and b as an array of JsonPatchOperations. +func diff(a, b map[string]interface{}, path string, patch []JsonPatchOperation) ([]JsonPatchOperation, error) { + for key, bv := range b { + p := makePath(path, key) + av, ok := a[key] + // value was added + if !ok { + patch = append(patch, NewPatch("add", p, bv)) + continue + } + // If types have changed, replace completely + if reflect.TypeOf(av) != reflect.TypeOf(bv) { + patch = append(patch, NewPatch("replace", p, bv)) + continue + } + // Types are the same, compare values + var err error + patch, err = handleValues(av, bv, p, patch) + if err != nil { + return nil, err + } + } + // Now add all deleted values as nil + for key := range a { + _, found := b[key] + if !found { + p := makePath(path, key) + + patch = append(patch, NewPatch("remove", p, nil)) + } + } + return patch, nil +} + +func handleValues(av, bv interface{}, p string, patch []JsonPatchOperation) ([]JsonPatchOperation, error) { + var err error + switch at := av.(type) { + case map[string]interface{}: + bt := bv.(map[string]interface{}) + patch, err = diff(at, bt, p, patch) + if err != nil { + return nil, err + } + case string, float64, bool: + if !matchesValue(av, bv) { + patch = append(patch, NewPatch("replace", p, bv)) + } + case []interface{}: + bt, ok := bv.([]interface{}) + if !ok { + // array replaced by non-array + patch = append(patch, NewPatch("replace", p, bv)) + } else if len(at) != len(bt) { + // arrays are not the same length + patch = append(patch, compareArray(at, bt, p)...) + + } else { + for i := range bt { + patch, err = handleValues(at[i], bt[i], makePath(p, i), patch) + if err != nil { + return nil, err + } + } + } + case nil: + switch bv.(type) { + case nil: + // Both nil, fine. + default: + patch = append(patch, NewPatch("add", p, bv)) + } + default: + panic(fmt.Sprintf("Unknown type:%T ", av)) + } + return patch, nil +} + +func compareArray(av, bv []interface{}, p string) []JsonPatchOperation { + retval := []JsonPatchOperation{} + // var err error + for i, v := range av { + found := false + for _, v2 := range bv { + if reflect.DeepEqual(v, v2) { + found = true + break + } + } + if !found { + retval = append(retval, NewPatch("remove", makePath(p, i), nil)) + } + } + + for i, v := range bv { + found := false + for _, v2 := range av { + if reflect.DeepEqual(v, v2) { + found = true + break + } + } + if !found { + retval = append(retval, NewPatch("add", makePath(p, i), v)) + } + } + + return retval +} From e0e567709d298ff8e04fa302c07486755f77dbdf Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Thu, 18 Oct 2018 15:00:30 +0200 Subject: [PATCH 04/23] Check for len(errs) not fore != nil --- pkg/admission/machinedeployments.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/admission/machinedeployments.go b/pkg/admission/machinedeployments.go index d21ed384b..2e8ba1e6d 100644 --- a/pkg/admission/machinedeployments.go +++ b/pkg/admission/machinedeployments.go @@ -30,7 +30,7 @@ func mutateMachineDeployments(ar admissionv1beta1.AdmissionReview) (*admissionv1 machineDeploymentOriginal := machineDeployment.DeepCopy() machineDeploymentDefaultingFunction(&machineDeployment) - if errs := validateMachineDeployment(machineDeployment); errs != nil { + if errs := validateMachineDeployment(machineDeployment); len(errs) > 0 { return nil, fmt.Errorf("validation failed: %v", errs) } From fdfdedf9ec79fd5082a93d4ddd91f091cd925809 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Thu, 18 Oct 2018 15:09:53 +0200 Subject: [PATCH 05/23] Only create a jsonpatch if there were changes --- pkg/admission/machinedeployments.go | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/pkg/admission/machinedeployments.go b/pkg/admission/machinedeployments.go index 2e8ba1e6d..6e2cc9a27 100644 --- a/pkg/admission/machinedeployments.go +++ b/pkg/admission/machinedeployments.go @@ -9,6 +9,7 @@ import ( "github.com/golang/glog" admissionv1beta1 "k8s.io/api/admission/v1beta1" + apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" @@ -34,20 +35,24 @@ func mutateMachineDeployments(ar admissionv1beta1.AdmissionReview) (*admissionv1 return nil, fmt.Errorf("validation failed: %v", errs) } - patchOpts, err := newJSONPatch(machineDeploymentOriginal, &machineDeployment) - if err != nil { - return nil, fmt.Errorf("failed to create json patch: %v", err) - } + response := &admissionv1beta1.AdmissionResponse{} + response.Allowed = true + if !apiequality.Semantic.DeepEqual(*machineDeploymentOriginal, machineDeployment) { + patchOpts, err := newJSONPatch(machineDeploymentOriginal, &machineDeployment) + if err != nil { + return nil, fmt.Errorf("failed to create json patch: %v", err) + } - patchRaw, err := json.Marshal(patchOpts) - if err != nil { - return nil, fmt.Errorf("failed to marshal json patch: %v", err) + patchRaw, err := json.Marshal(patchOpts) + if err != nil { + return nil, fmt.Errorf("failed to marshal json patch: %v", err) + } + glog.V(6).Infof("Produced jsonpatch: %s", string(patchRaw)) + + response.Patch = patchRaw + response.PatchType = &jsonPatch } - response := &admissionv1beta1.AdmissionResponse{} - response.Allowed = true - response.Patch = patchRaw - response.PatchType = &jsonPatch return response, nil } From d42544622ea17a9a5d12f457bc09b622a81c57ff Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Thu, 18 Oct 2018 15:37:36 +0200 Subject: [PATCH 06/23] Use evanphx/json-patch, hopefully not buggy --- Gopkg.lock | 10 +- pkg/admission/admission.go | 4 +- vendor/github.com/mattbaird/jsonpatch/LICENSE | 202 -------------- .../mattbaird/jsonpatch/jsonpatch.go | 257 ------------------ 4 files changed, 3 insertions(+), 470 deletions(-) delete mode 100644 vendor/github.com/mattbaird/jsonpatch/LICENSE delete mode 100644 vendor/github.com/mattbaird/jsonpatch/jsonpatch.go diff --git a/Gopkg.lock b/Gopkg.lock index 0c553abfb..a653380ba 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -552,14 +552,6 @@ pruneopts = "NUT" revision = "03f2033d19d5860aef995fe360ac7d395cd8ce65" -[[projects]] - branch = "master" - digest = "1:0e9bfc47ab9941ecc3344e580baca5deb4091177e84dd9773b48b38ec26b93d5" - name = "github.com/mattbaird/jsonpatch" - packages = ["."] - pruneopts = "NUT" - revision = "81af80346b1a01caae0cbc27fd3c1ba5b11e189f" - [[projects]] digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6" name = "github.com/matttproud/golang_protobuf_extensions" @@ -1382,6 +1374,7 @@ "github.com/aws/aws-sdk-go/service/iam", "github.com/coreos/container-linux-config-transpiler/config", "github.com/digitalocean/godo", + "github.com/evanphx/json-patch", "github.com/ghodss/yaml", "github.com/go-test/deep", "github.com/golang/glog", @@ -1403,7 +1396,6 @@ "github.com/gophercloud/gophercloud/pagination", "github.com/heptiolabs/healthcheck", "github.com/hetznercloud/hcloud-go/hcloud", - "github.com/mattbaird/jsonpatch", "github.com/oklog/run", "github.com/pborman/uuid", "github.com/pmezard/go-difflib/difflib", diff --git a/pkg/admission/admission.go b/pkg/admission/admission.go index 825affa90..ce9159c3d 100644 --- a/pkg/admission/admission.go +++ b/pkg/admission/admission.go @@ -7,7 +7,7 @@ import ( "reflect" "time" - "github.com/mattbaird/jsonpatch" + jsonpatch "github.com/evanphx/json-patch" "k8s.io/apimachinery/pkg/runtime" ) @@ -37,5 +37,5 @@ func newJSONPatch(original, current runtime.Object) ([]jsonpatch.JsonPatchOperat if err != nil { return nil, err } - return jsonpatch.CreatePatch(ori, cur) + return jsonpatch.CreateMergePatch(ori, cur) } diff --git a/vendor/github.com/mattbaird/jsonpatch/LICENSE b/vendor/github.com/mattbaird/jsonpatch/LICENSE deleted file mode 100644 index 8f71f43fe..000000000 --- a/vendor/github.com/mattbaird/jsonpatch/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/vendor/github.com/mattbaird/jsonpatch/jsonpatch.go b/vendor/github.com/mattbaird/jsonpatch/jsonpatch.go deleted file mode 100644 index 295f260f5..000000000 --- a/vendor/github.com/mattbaird/jsonpatch/jsonpatch.go +++ /dev/null @@ -1,257 +0,0 @@ -package jsonpatch - -import ( - "bytes" - "encoding/json" - "fmt" - "reflect" - "strings" -) - -var errBadJSONDoc = fmt.Errorf("Invalid JSON Document") - -type JsonPatchOperation struct { - Operation string `json:"op"` - Path string `json:"path"` - Value interface{} `json:"value,omitempty"` -} - -func (j *JsonPatchOperation) Json() string { - b, _ := json.Marshal(j) - return string(b) -} - -func (j *JsonPatchOperation) MarshalJSON() ([]byte, error) { - var b bytes.Buffer - b.WriteString("{") - b.WriteString(fmt.Sprintf(`"op":"%s"`, j.Operation)) - b.WriteString(fmt.Sprintf(`,"path":"%s"`, j.Path)) - // Consider omitting Value for non-nullable operations. - if j.Value != nil || j.Operation == "replace" || j.Operation == "add" { - v, err := json.Marshal(j.Value) - if err != nil { - return nil, err - } - b.WriteString(`,"value":`) - b.Write(v) - } - b.WriteString("}") - return b.Bytes(), nil -} - -type ByPath []JsonPatchOperation - -func (a ByPath) Len() int { return len(a) } -func (a ByPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ByPath) Less(i, j int) bool { return a[i].Path < a[j].Path } - -func NewPatch(operation, path string, value interface{}) JsonPatchOperation { - return JsonPatchOperation{Operation: operation, Path: path, Value: value} -} - -// CreatePatch creates a patch as specified in http://jsonpatch.com/ -// -// 'a' is original, 'b' is the modified document. Both are to be given as json encoded content. -// The function will return an array of JsonPatchOperations -// -// An error will be returned if any of the two documents are invalid. -func CreatePatch(a, b []byte) ([]JsonPatchOperation, error) { - aI := map[string]interface{}{} - bI := map[string]interface{}{} - err := json.Unmarshal(a, &aI) - if err != nil { - return nil, errBadJSONDoc - } - err = json.Unmarshal(b, &bI) - if err != nil { - return nil, errBadJSONDoc - } - return diff(aI, bI, "", []JsonPatchOperation{}) -} - -// Returns true if the values matches (must be json types) -// The types of the values must match, otherwise it will always return false -// If two map[string]interface{} are given, all elements must match. -func matchesValue(av, bv interface{}) bool { - if reflect.TypeOf(av) != reflect.TypeOf(bv) { - return false - } - switch at := av.(type) { - case string: - bt := bv.(string) - if bt == at { - return true - } - case float64: - bt := bv.(float64) - if bt == at { - return true - } - case bool: - bt := bv.(bool) - if bt == at { - return true - } - case map[string]interface{}: - bt := bv.(map[string]interface{}) - for key := range at { - if !matchesValue(at[key], bt[key]) { - return false - } - } - for key := range bt { - if !matchesValue(at[key], bt[key]) { - return false - } - } - return true - case []interface{}: - bt := bv.([]interface{}) - if len(bt) != len(at) { - return false - } - for key := range at { - if !matchesValue(at[key], bt[key]) { - return false - } - } - for key := range bt { - if !matchesValue(at[key], bt[key]) { - return false - } - } - return true - } - return false -} - -// From http://tools.ietf.org/html/rfc6901#section-4 : -// -// Evaluation of each reference token begins by decoding any escaped -// character sequence. This is performed by first transforming any -// occurrence of the sequence '~1' to '/', and then transforming any -// occurrence of the sequence '~0' to '~'. -// TODO decode support: -// var rfc6901Decoder = strings.NewReplacer("~1", "/", "~0", "~") - -var rfc6901Encoder = strings.NewReplacer("~", "~0", "/", "~1") - -func makePath(path string, newPart interface{}) string { - key := rfc6901Encoder.Replace(fmt.Sprintf("%v", newPart)) - if path == "" { - return "/" + key - } - if strings.HasSuffix(path, "/") { - return path + key - } - return path + "/" + key -} - -// diff returns the (recursive) difference between a and b as an array of JsonPatchOperations. -func diff(a, b map[string]interface{}, path string, patch []JsonPatchOperation) ([]JsonPatchOperation, error) { - for key, bv := range b { - p := makePath(path, key) - av, ok := a[key] - // value was added - if !ok { - patch = append(patch, NewPatch("add", p, bv)) - continue - } - // If types have changed, replace completely - if reflect.TypeOf(av) != reflect.TypeOf(bv) { - patch = append(patch, NewPatch("replace", p, bv)) - continue - } - // Types are the same, compare values - var err error - patch, err = handleValues(av, bv, p, patch) - if err != nil { - return nil, err - } - } - // Now add all deleted values as nil - for key := range a { - _, found := b[key] - if !found { - p := makePath(path, key) - - patch = append(patch, NewPatch("remove", p, nil)) - } - } - return patch, nil -} - -func handleValues(av, bv interface{}, p string, patch []JsonPatchOperation) ([]JsonPatchOperation, error) { - var err error - switch at := av.(type) { - case map[string]interface{}: - bt := bv.(map[string]interface{}) - patch, err = diff(at, bt, p, patch) - if err != nil { - return nil, err - } - case string, float64, bool: - if !matchesValue(av, bv) { - patch = append(patch, NewPatch("replace", p, bv)) - } - case []interface{}: - bt, ok := bv.([]interface{}) - if !ok { - // array replaced by non-array - patch = append(patch, NewPatch("replace", p, bv)) - } else if len(at) != len(bt) { - // arrays are not the same length - patch = append(patch, compareArray(at, bt, p)...) - - } else { - for i := range bt { - patch, err = handleValues(at[i], bt[i], makePath(p, i), patch) - if err != nil { - return nil, err - } - } - } - case nil: - switch bv.(type) { - case nil: - // Both nil, fine. - default: - patch = append(patch, NewPatch("add", p, bv)) - } - default: - panic(fmt.Sprintf("Unknown type:%T ", av)) - } - return patch, nil -} - -func compareArray(av, bv []interface{}, p string) []JsonPatchOperation { - retval := []JsonPatchOperation{} - // var err error - for i, v := range av { - found := false - for _, v2 := range bv { - if reflect.DeepEqual(v, v2) { - found = true - break - } - } - if !found { - retval = append(retval, NewPatch("remove", makePath(p, i), nil)) - } - } - - for i, v := range bv { - found := false - for _, v2 := range av { - if reflect.DeepEqual(v, v2) { - found = true - break - } - } - if !found { - retval = append(retval, NewPatch("add", makePath(p, i), v)) - } - } - - return retval -} From 310f1be3eab392c4f509331de0f15b240e68af9a Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Thu, 18 Oct 2018 16:01:30 +0200 Subject: [PATCH 07/23] Test --- pkg/admission/admission.go | 6 ++---- pkg/admission/machinedeployments.go | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/admission/admission.go b/pkg/admission/admission.go index ce9159c3d..1237156b1 100644 --- a/pkg/admission/admission.go +++ b/pkg/admission/admission.go @@ -1,13 +1,11 @@ package admission import ( - "encoding/json" - "fmt" "net/http" "reflect" "time" - jsonpatch "github.com/evanphx/json-patch" + "github.com/mattbaird/jsonpatch" "k8s.io/apimachinery/pkg/runtime" ) @@ -37,5 +35,5 @@ func newJSONPatch(original, current runtime.Object) ([]jsonpatch.JsonPatchOperat if err != nil { return nil, err } - return jsonpatch.CreateMergePatch(ori, cur) + return jsonpatch.CreatePatch(ori, cur) } diff --git a/pkg/admission/machinedeployments.go b/pkg/admission/machinedeployments.go index 6e2cc9a27..5859764a0 100644 --- a/pkg/admission/machinedeployments.go +++ b/pkg/admission/machinedeployments.go @@ -47,6 +47,7 @@ func mutateMachineDeployments(ar admissionv1beta1.AdmissionReview) (*admissionv1 if err != nil { return nil, fmt.Errorf("failed to marshal json patch: %v", err) } + patchRaw = []byte(`[{"op":"add","path":"/spec","value":{"strategy":{"rollingUpdate":{"maxSurge":1,"maxUnavailable":0},"type":"RollingUpdate"}}}]`) glog.V(6).Infof("Produced jsonpatch: %s", string(patchRaw)) response.Patch = patchRaw From 09af17fdcaa7cb4d1e617bd93abcd8a634a94da7 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Thu, 18 Oct 2018 18:02:17 +0200 Subject: [PATCH 08/23] Revert back to mattbairds jsonpatch library --- Gopkg.lock | 10 +- pkg/admission/admission.go | 2 + pkg/admission/machinedeployments.go | 1 - vendor/github.com/mattbaird/jsonpatch/LICENSE | 202 ++++++++++++++ .../mattbaird/jsonpatch/jsonpatch.go | 257 ++++++++++++++++++ 5 files changed, 470 insertions(+), 2 deletions(-) create mode 100644 vendor/github.com/mattbaird/jsonpatch/LICENSE create mode 100644 vendor/github.com/mattbaird/jsonpatch/jsonpatch.go diff --git a/Gopkg.lock b/Gopkg.lock index a653380ba..0c553abfb 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -552,6 +552,14 @@ pruneopts = "NUT" revision = "03f2033d19d5860aef995fe360ac7d395cd8ce65" +[[projects]] + branch = "master" + digest = "1:0e9bfc47ab9941ecc3344e580baca5deb4091177e84dd9773b48b38ec26b93d5" + name = "github.com/mattbaird/jsonpatch" + packages = ["."] + pruneopts = "NUT" + revision = "81af80346b1a01caae0cbc27fd3c1ba5b11e189f" + [[projects]] digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6" name = "github.com/matttproud/golang_protobuf_extensions" @@ -1374,7 +1382,6 @@ "github.com/aws/aws-sdk-go/service/iam", "github.com/coreos/container-linux-config-transpiler/config", "github.com/digitalocean/godo", - "github.com/evanphx/json-patch", "github.com/ghodss/yaml", "github.com/go-test/deep", "github.com/golang/glog", @@ -1396,6 +1403,7 @@ "github.com/gophercloud/gophercloud/pagination", "github.com/heptiolabs/healthcheck", "github.com/hetznercloud/hcloud-go/hcloud", + "github.com/mattbaird/jsonpatch", "github.com/oklog/run", "github.com/pborman/uuid", "github.com/pmezard/go-difflib/difflib", diff --git a/pkg/admission/admission.go b/pkg/admission/admission.go index 1237156b1..825affa90 100644 --- a/pkg/admission/admission.go +++ b/pkg/admission/admission.go @@ -1,6 +1,8 @@ package admission import ( + "encoding/json" + "fmt" "net/http" "reflect" "time" diff --git a/pkg/admission/machinedeployments.go b/pkg/admission/machinedeployments.go index 5859764a0..6e2cc9a27 100644 --- a/pkg/admission/machinedeployments.go +++ b/pkg/admission/machinedeployments.go @@ -47,7 +47,6 @@ func mutateMachineDeployments(ar admissionv1beta1.AdmissionReview) (*admissionv1 if err != nil { return nil, fmt.Errorf("failed to marshal json patch: %v", err) } - patchRaw = []byte(`[{"op":"add","path":"/spec","value":{"strategy":{"rollingUpdate":{"maxSurge":1,"maxUnavailable":0},"type":"RollingUpdate"}}}]`) glog.V(6).Infof("Produced jsonpatch: %s", string(patchRaw)) response.Patch = patchRaw diff --git a/vendor/github.com/mattbaird/jsonpatch/LICENSE b/vendor/github.com/mattbaird/jsonpatch/LICENSE new file mode 100644 index 000000000..8f71f43fe --- /dev/null +++ b/vendor/github.com/mattbaird/jsonpatch/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/vendor/github.com/mattbaird/jsonpatch/jsonpatch.go b/vendor/github.com/mattbaird/jsonpatch/jsonpatch.go new file mode 100644 index 000000000..295f260f5 --- /dev/null +++ b/vendor/github.com/mattbaird/jsonpatch/jsonpatch.go @@ -0,0 +1,257 @@ +package jsonpatch + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" +) + +var errBadJSONDoc = fmt.Errorf("Invalid JSON Document") + +type JsonPatchOperation struct { + Operation string `json:"op"` + Path string `json:"path"` + Value interface{} `json:"value,omitempty"` +} + +func (j *JsonPatchOperation) Json() string { + b, _ := json.Marshal(j) + return string(b) +} + +func (j *JsonPatchOperation) MarshalJSON() ([]byte, error) { + var b bytes.Buffer + b.WriteString("{") + b.WriteString(fmt.Sprintf(`"op":"%s"`, j.Operation)) + b.WriteString(fmt.Sprintf(`,"path":"%s"`, j.Path)) + // Consider omitting Value for non-nullable operations. + if j.Value != nil || j.Operation == "replace" || j.Operation == "add" { + v, err := json.Marshal(j.Value) + if err != nil { + return nil, err + } + b.WriteString(`,"value":`) + b.Write(v) + } + b.WriteString("}") + return b.Bytes(), nil +} + +type ByPath []JsonPatchOperation + +func (a ByPath) Len() int { return len(a) } +func (a ByPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByPath) Less(i, j int) bool { return a[i].Path < a[j].Path } + +func NewPatch(operation, path string, value interface{}) JsonPatchOperation { + return JsonPatchOperation{Operation: operation, Path: path, Value: value} +} + +// CreatePatch creates a patch as specified in http://jsonpatch.com/ +// +// 'a' is original, 'b' is the modified document. Both are to be given as json encoded content. +// The function will return an array of JsonPatchOperations +// +// An error will be returned if any of the two documents are invalid. +func CreatePatch(a, b []byte) ([]JsonPatchOperation, error) { + aI := map[string]interface{}{} + bI := map[string]interface{}{} + err := json.Unmarshal(a, &aI) + if err != nil { + return nil, errBadJSONDoc + } + err = json.Unmarshal(b, &bI) + if err != nil { + return nil, errBadJSONDoc + } + return diff(aI, bI, "", []JsonPatchOperation{}) +} + +// Returns true if the values matches (must be json types) +// The types of the values must match, otherwise it will always return false +// If two map[string]interface{} are given, all elements must match. +func matchesValue(av, bv interface{}) bool { + if reflect.TypeOf(av) != reflect.TypeOf(bv) { + return false + } + switch at := av.(type) { + case string: + bt := bv.(string) + if bt == at { + return true + } + case float64: + bt := bv.(float64) + if bt == at { + return true + } + case bool: + bt := bv.(bool) + if bt == at { + return true + } + case map[string]interface{}: + bt := bv.(map[string]interface{}) + for key := range at { + if !matchesValue(at[key], bt[key]) { + return false + } + } + for key := range bt { + if !matchesValue(at[key], bt[key]) { + return false + } + } + return true + case []interface{}: + bt := bv.([]interface{}) + if len(bt) != len(at) { + return false + } + for key := range at { + if !matchesValue(at[key], bt[key]) { + return false + } + } + for key := range bt { + if !matchesValue(at[key], bt[key]) { + return false + } + } + return true + } + return false +} + +// From http://tools.ietf.org/html/rfc6901#section-4 : +// +// Evaluation of each reference token begins by decoding any escaped +// character sequence. This is performed by first transforming any +// occurrence of the sequence '~1' to '/', and then transforming any +// occurrence of the sequence '~0' to '~'. +// TODO decode support: +// var rfc6901Decoder = strings.NewReplacer("~1", "/", "~0", "~") + +var rfc6901Encoder = strings.NewReplacer("~", "~0", "/", "~1") + +func makePath(path string, newPart interface{}) string { + key := rfc6901Encoder.Replace(fmt.Sprintf("%v", newPart)) + if path == "" { + return "/" + key + } + if strings.HasSuffix(path, "/") { + return path + key + } + return path + "/" + key +} + +// diff returns the (recursive) difference between a and b as an array of JsonPatchOperations. +func diff(a, b map[string]interface{}, path string, patch []JsonPatchOperation) ([]JsonPatchOperation, error) { + for key, bv := range b { + p := makePath(path, key) + av, ok := a[key] + // value was added + if !ok { + patch = append(patch, NewPatch("add", p, bv)) + continue + } + // If types have changed, replace completely + if reflect.TypeOf(av) != reflect.TypeOf(bv) { + patch = append(patch, NewPatch("replace", p, bv)) + continue + } + // Types are the same, compare values + var err error + patch, err = handleValues(av, bv, p, patch) + if err != nil { + return nil, err + } + } + // Now add all deleted values as nil + for key := range a { + _, found := b[key] + if !found { + p := makePath(path, key) + + patch = append(patch, NewPatch("remove", p, nil)) + } + } + return patch, nil +} + +func handleValues(av, bv interface{}, p string, patch []JsonPatchOperation) ([]JsonPatchOperation, error) { + var err error + switch at := av.(type) { + case map[string]interface{}: + bt := bv.(map[string]interface{}) + patch, err = diff(at, bt, p, patch) + if err != nil { + return nil, err + } + case string, float64, bool: + if !matchesValue(av, bv) { + patch = append(patch, NewPatch("replace", p, bv)) + } + case []interface{}: + bt, ok := bv.([]interface{}) + if !ok { + // array replaced by non-array + patch = append(patch, NewPatch("replace", p, bv)) + } else if len(at) != len(bt) { + // arrays are not the same length + patch = append(patch, compareArray(at, bt, p)...) + + } else { + for i := range bt { + patch, err = handleValues(at[i], bt[i], makePath(p, i), patch) + if err != nil { + return nil, err + } + } + } + case nil: + switch bv.(type) { + case nil: + // Both nil, fine. + default: + patch = append(patch, NewPatch("add", p, bv)) + } + default: + panic(fmt.Sprintf("Unknown type:%T ", av)) + } + return patch, nil +} + +func compareArray(av, bv []interface{}, p string) []JsonPatchOperation { + retval := []JsonPatchOperation{} + // var err error + for i, v := range av { + found := false + for _, v2 := range bv { + if reflect.DeepEqual(v, v2) { + found = true + break + } + } + if !found { + retval = append(retval, NewPatch("remove", makePath(p, i), nil)) + } + } + + for i, v := range bv { + found := false + for _, v2 := range av { + if reflect.DeepEqual(v, v2) { + found = true + break + } + } + if !found { + retval = append(retval, NewPatch("add", makePath(p, i), v)) + } + } + + return retval +} From 340a9bd7ca4dd09767fcb264eeb7b045a1c94cf8 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Thu, 18 Oct 2018 18:11:30 +0200 Subject: [PATCH 09/23] Add some debug logging --- pkg/admission/admission.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/admission/admission.go b/pkg/admission/admission.go index 825affa90..09654c7af 100644 --- a/pkg/admission/admission.go +++ b/pkg/admission/admission.go @@ -7,6 +7,7 @@ import ( "reflect" "time" + "github.com/golang/glog" "github.com/mattbaird/jsonpatch" "k8s.io/apimachinery/pkg/runtime" @@ -33,9 +34,11 @@ func newJSONPatch(original, current runtime.Object) ([]jsonpatch.JsonPatchOperat if err != nil { return nil, err } + glog.V(4).Infof("jsonpatch: Marshaled original: %s", string(ori)) cur, err := json.Marshal(current) if err != nil { return nil, err } + glog.V(4).Infof("jsonpatch: Marshaled target: %s", string(cur)) return jsonpatch.CreatePatch(ori, cur) } From ae4db04283ee5847cf34f06680e5a0a23adcaf32 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Thu, 18 Oct 2018 19:24:29 +0200 Subject: [PATCH 10/23] Update e2e testing to use MatatingAdmissionWebhook --- test/tools/integration/provision_master.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/tools/integration/provision_master.sh b/test/tools/integration/provision_master.sh index 16ec72f72..db42c0632 100755 --- a/test/tools/integration/provision_master.sh +++ b/test/tools/integration/provision_master.sh @@ -16,8 +16,8 @@ for try in {1..100}; do done -rsync -av -e "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" \ - ../../../{examples/machine-controller.yaml,machine-controller,Dockerfile} \ +rsync -avR -e "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" \ + ../../.././{Makefile,examples/machine-controller.yaml,machine-controller,Dockerfile} \ root@$ADDR:/root/ cat < Date: Fri, 19 Oct 2018 09:11:02 +0200 Subject: [PATCH 11/23] Increase size of e2e controlller to check if it helps --- test/tools/integration/hetzner.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tools/integration/hetzner.tf b/test/tools/integration/hetzner.tf index 3d6dd4d58..3f72bfa4a 100644 --- a/test/tools/integration/hetzner.tf +++ b/test/tools/integration/hetzner.tf @@ -10,6 +10,6 @@ resource "hcloud_ssh_key" "default" { resource "hcloud_server" "machine-controller-test" { name = "${var.hcloud_test_server_name}" image = "ubuntu-18.04" - server_type = "cx41" + server_type = "cx51" ssh_keys = ["${hcloud_ssh_key.default.id}"] } From 4b7aef79553fdd161bedbb96e42e1b5a1a596955 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Fri, 19 Oct 2018 10:21:40 +0200 Subject: [PATCH 12/23] Use ipvs on kubeadm-controller in e2e tests --- test/tools/integration/provision_master.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/tools/integration/provision_master.sh b/test/tools/integration/provision_master.sh index db42c0632..9688f664e 100755 --- a/test/tools/integration/provision_master.sh +++ b/test/tools/integration/provision_master.sh @@ -56,10 +56,10 @@ EOF kubeadm init --kubernetes-version=v1.12.0 --apiserver-advertise-address=$ADDR --pod-network-cidr=10.244.0.0/16 sed -i 's/\(.*leader-elect=true\)/\1\n - --feature-gates=ScheduleDaemonSetPods=false/g' /etc/kubernetes/manifests/kube-scheduler.yaml sed -i 's/\(.*leader-elect=true\)/\1\n - --feature-gates=ScheduleDaemonSetPods=false/g' /etc/kubernetes/manifests/kube-controller-manager.yaml -fi -if ! ls \$HOME/.kube/config; then + modprobe ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh nf_conntrack_ipv4 mkdir -p \$HOME/.kube cp -i /etc/kubernetes/admin.conf \$HOME/.kube/config + kubectl get configmap -n kube-system kube-proxy -o yaml|sed 's/mode: ""/mode: "ipvs"/g'|kubectl apply -f - kubectl taint nodes --all node-role.kubernetes.io/master- kubectl get configmap -n kube-system kubelet-config-1.12 -o yaml \ |sed '/creationTimestamp/d;/resourceVersion/d;/selfLink/d;/uid/d;s/kubelet-config-1.12/kubelet-config-1.11/g' \ From c03b573ba85821d9fc61d2fffe064f3e7c08e979 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Fri, 19 Oct 2018 11:20:30 +0200 Subject: [PATCH 13/23] Revert "Use ipvs on kubeadm-controller in e2e tests" This reverts commit 4b7aef79553fdd161bedbb96e42e1b5a1a596955. --- test/tools/integration/provision_master.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/tools/integration/provision_master.sh b/test/tools/integration/provision_master.sh index 9688f664e..db42c0632 100755 --- a/test/tools/integration/provision_master.sh +++ b/test/tools/integration/provision_master.sh @@ -56,10 +56,10 @@ EOF kubeadm init --kubernetes-version=v1.12.0 --apiserver-advertise-address=$ADDR --pod-network-cidr=10.244.0.0/16 sed -i 's/\(.*leader-elect=true\)/\1\n - --feature-gates=ScheduleDaemonSetPods=false/g' /etc/kubernetes/manifests/kube-scheduler.yaml sed -i 's/\(.*leader-elect=true\)/\1\n - --feature-gates=ScheduleDaemonSetPods=false/g' /etc/kubernetes/manifests/kube-controller-manager.yaml - modprobe ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh nf_conntrack_ipv4 +fi +if ! ls \$HOME/.kube/config; then mkdir -p \$HOME/.kube cp -i /etc/kubernetes/admin.conf \$HOME/.kube/config - kubectl get configmap -n kube-system kube-proxy -o yaml|sed 's/mode: ""/mode: "ipvs"/g'|kubectl apply -f - kubectl taint nodes --all node-role.kubernetes.io/master- kubectl get configmap -n kube-system kubelet-config-1.12 -o yaml \ |sed '/creationTimestamp/d;/resourceVersion/d;/selfLink/d;/uid/d;s/kubelet-config-1.12/kubelet-config-1.11/g' \ From 69a36e77f758b66eccbdec734bd1c90781efe449 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Fri, 19 Oct 2018 13:08:15 +0200 Subject: [PATCH 14/23] Move webhook into a dedicated app --- .circleci/config.yml | 2 ++ .gitignore | 1 + Dockerfile.webhook | 5 ++++ Makefile | 19 ++++++++++++- cmd/controller/main.go | 32 ++++------------------ cmd/webhook/main.go | 26 ++++++++++++++++++ examples/machine-controller.yaml | 15 +++++++--- test/tools/integration/provision_master.sh | 6 ++-- 8 files changed, 72 insertions(+), 34 deletions(-) create mode 100644 Dockerfile.webhook create mode 100644 cmd/webhook/main.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 3a59d7a2b..48fa9cfeb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -48,10 +48,12 @@ jobs: key: repo-{{ .Environment.CIRCLE_SHA1 }} - run: DEP_RELEASE_TAG=v0.5.0 curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - run: make machine-controller + - run: make webhook - save_cache: key: machine-controller-{{ .Revision }} paths: - /go/src/github.com/kubermatic/machine-controller + - /go/src/github.com/kubermatic/webhook end-to-end: <<: *defaults steps: diff --git a/.gitignore b/.gitignore index d1c0eb92c..49946523b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ terraform-provider-hcloud examples/*.pem examples/*.csr examples/*.srl +./webhook diff --git a/Dockerfile.webhook b/Dockerfile.webhook new file mode 100644 index 000000000..57e131c52 --- /dev/null +++ b/Dockerfile.webhook @@ -0,0 +1,5 @@ +FROM alpine:3.7 + +COPY webhook /usr/local/bin + +USER nobody diff --git a/Makefile b/Makefile index 5d79d3c14..8f86f0c4f 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ REGISTRY_NAMESPACE ?= kubermatic IMAGE_TAG = \ $(shell echo $$(git rev-parse HEAD && if [[ -n $$(git status --porcelain) ]]; then echo '-dirty'; fi)|tr -d ' ') IMAGE_NAME = $(REGISTRY)/$(REGISTRY_NAMESPACE)/machine-controller:$(IMAGE_TAG) +IMAGE_NAME_WEBHOOK = $(REGISTRY)/$(REGISTRY_NAMESPACE)/machine-controller-webhook:$(IMAGE_TAG) vendor: Gopkg.lock Gopkg.toml @@ -30,7 +31,13 @@ machine-controller: $(shell find cmd pkg -name '*.go') vendor -o machine-controller \ github.com/kubermatic/machine-controller/cmd/controller -docker-image: machine-controller docker-image-nodep +webhook: $(shell find cmd pkg -name '*.go') vendor + go build -v \ + -ldflags '-s -w' \ + -o webhook \ + github.com/kubermatic/machine-controller/cmd/webhook + +docker-image: machine-controller docker-image-nodep admission-webhook # This target exists because in our CI # we do not want to restore the vendor @@ -47,6 +54,16 @@ docker-image-nodep: docker build -t $(IMAGE_NAME) . ;\ docker push $(IMAGE_NAME) ;\ fi + docker build -t $(IMAGE_NAME_WEBHOOK) -f Dockerfile.webhook . + docker push $(IMAGE_NAME_WEBHOOK) + if [[ -n "$(GIT_TAG)" ]]; then \ + $(eval IMAGE_TAG = $(GIT_TAG)) \ + docker build -t $(IMAGE_NAME_WEBHOOK) -f Dockerfile.webhook . && \ + docker push $(IMAGE_NAME_WEBHOOK) && \ + $(eval IMAGE_TAG = latest) \ + docker build -t $(IMAGE_NAME_WEBHOOK) -f Dockerfile.webhook . ;\ + docker push $(IMAGE_NAME_WEBHOOK) ;\ + fi test-unit-docker: @docker run --rm \ diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 641696e87..eb2782b58 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -48,7 +48,6 @@ import ( "github.com/golang/glog" "github.com/heptiolabs/healthcheck" - "github.com/kubermatic/machine-controller/pkg/admission" "github.com/kubermatic/machine-controller/pkg/apis/cluster/v1alpha1/migrations" "github.com/kubermatic/machine-controller/pkg/clusterinfo" machinecontroller "github.com/kubermatic/machine-controller/pkg/controller/machine" @@ -67,15 +66,12 @@ import ( ) var ( - masterURL string - kubeconfig string - clusterDNSIPs string - listenAddress string - admissionListenAddress string - admissionTLSCertPath string - admissionTLSKeyPath string - name string - workerCount int + masterURL string + kubeconfig string + clusterDNSIPs string + listenAddress string + name string + workerCount int ) const ( @@ -152,9 +148,6 @@ func main() { flag.IntVar(&workerCount, "worker-count", 5, "Number of workers to process machines. Using a high number with a lot of machines might cause getting rate-limited from your cloud provider.") flag.StringVar(&listenAddress, "internal-listen-address", "127.0.0.1:8085", "The address on which the http server will listen on. The server exposes metrics on /metrics, liveness check on /live and readiness check on /ready") flag.StringVar(&name, "name", "", "When set, the controller will only process machines with the label \"machine.k8s.io/controller\": name") - flag.StringVar(&admissionListenAddress, "admission-listen-address", ":9876", "The address on which the MutatingWebhook will listen on") - flag.StringVar(&admissionTLSCertPath, "admission-tls-cert-path", "/tmp/cert/cert.pem", "The path of the TLS cert for the MutatingWebhook") - flag.StringVar(&admissionTLSKeyPath, "admission-tls-key-path", "/tmp/cert/key.pem", "The path of the TLS key for the MutatingWebhook") flag.Parse() @@ -275,19 +268,6 @@ func main() { } }) } - { - s := admission.New(admissionListenAddress) - g.Add(func() error { - return s.ListenAndServeTLS(admissionTLSCertPath, admissionTLSKeyPath) - }, func(err error) { - glog.Warningf("shutting down admission HTTP server due to: %s", err) - srvCtx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - if err = s.Shutdown(srvCtx); err != nil { - glog.Errorf("failed to shutdown admission HTTP server: %s", err) - } - }) - } { g.Add(func() error { select { diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go new file mode 100644 index 000000000..77265be0c --- /dev/null +++ b/cmd/webhook/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "flag" + + "github.com/golang/glog" + + "github.com/kubermatic/machine-controller/pkg/admission" +) + +var ( + admissionListenAddress string + admissionTLSCertPath string + admissionTLSKeyPath string +) + +func main() { + flag.StringVar(&admissionListenAddress, "listen-address", ":9876", "The address on which the MutatingWebhook will listen on") + flag.StringVar(&admissionTLSCertPath, "tls-cert-path", "/tmp/cert/cert.pem", "The path of the TLS cert for the MutatingWebhook") + flag.StringVar(&admissionTLSKeyPath, "tls-key-path", "/tmp/cert/key.pem", "The path of the TLS key for the MutatingWebhook") + flag.Parse() + + s := admission.New(admissionListenAddress) + glog.Infof("Starting to listen on %s", admissionListenAddress) + glog.Fatal(s.ListenAndServeTLS(admissionTLSCertPath, admissionTLSKeyPath)) +} diff --git a/examples/machine-controller.yaml b/examples/machine-controller.yaml index ecdffd873..cc81bbd5a 100644 --- a/examples/machine-controller.yaml +++ b/examples/machine-controller.yaml @@ -165,6 +165,17 @@ spec: spec: serviceAccountName: machine-controller containers: + - image: kubermatic/machine-controller-webhook:latest + imagePullPolicy: IfNotPresent + name: webhook + command: + - /usr/local/bin/webhook + - -logtostderr + - -v=6 + - listen-address=0.0.0.0:9876 + volumeMounts: + - name: machine-controller-admission-cert + mountPath: /tmp/cert - image: kubermatic/machine-controller:latest imagePullPolicy: IfNotPresent name: machine-controller @@ -175,7 +186,6 @@ spec: - -worker-count=5 - -cluster-dns=10.10.10.10 - -internal-listen-address=0.0.0.0:8085 - - -admission-listen-address=0.0.0.0:9876 ports: - containerPort: 8085 livenessProbe: @@ -189,9 +199,6 @@ spec: path: /ready port: 8085 periodSeconds: 5 - volumeMounts: - - name: machine-controller-admission-cert - mountPath: /tmp/cert volumes: - name: machine-controller-admission-cert secret: diff --git a/test/tools/integration/provision_master.sh b/test/tools/integration/provision_master.sh index db42c0632..816966aee 100755 --- a/test/tools/integration/provision_master.sh +++ b/test/tools/integration/provision_master.sh @@ -15,9 +15,8 @@ for try in {1..100}; do sleep 1; done - rsync -avR -e "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" \ - ../../.././{Makefile,examples/machine-controller.yaml,machine-controller,Dockerfile} \ + ../../.././{Makefile,examples/machine-controller.yaml,machine-controller,Dockerfile,webhook,Dockerfile.webhook} \ root@$ADDR:/root/ cat < Date: Fri, 19 Oct 2018 15:05:34 +0200 Subject: [PATCH 15/23] Move webhook into a dedicated deployment --- .gitignore | 3 +- examples/machine-controller.yaml | 54 ++++++++++++++++++++++------- pkg/admission/admission.go | 5 +++ pkg/admission/machinedeployments.go | 1 + 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 49946523b..b5adba2bb 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ terraform-provider-hcloud examples/*.pem examples/*.csr examples/*.srl -./webhook +webhook +!cmd/webhook diff --git a/examples/machine-controller.yaml b/examples/machine-controller.yaml index cc81bbd5a..55e5770f9 100644 --- a/examples/machine-controller.yaml +++ b/examples/machine-controller.yaml @@ -165,17 +165,6 @@ spec: spec: serviceAccountName: machine-controller containers: - - image: kubermatic/machine-controller-webhook:latest - imagePullPolicy: IfNotPresent - name: webhook - command: - - /usr/local/bin/webhook - - -logtostderr - - -v=6 - - listen-address=0.0.0.0:9876 - volumeMounts: - - name: machine-controller-admission-cert - mountPath: /tmp/cert - image: kubermatic/machine-controller:latest imagePullPolicy: IfNotPresent name: machine-controller @@ -199,6 +188,45 @@ spec: path: /ready port: 8085 periodSeconds: 5 +--- +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: machine-controller-webhook + namespace: kube-system +spec: + replicas: 2 + selector: + matchLabels: + app: machine-controller-webhook + template: + metadata: + labels: + app: machine-controller-webhook + spec: + containers: + - image: kubermatic/machine-controller-webhook:latest + imagePullPolicy: IfNotPresent + name: webhook + command: + - /usr/local/bin/webhook + - -logtostderr + - -v=6 + - -listen-address=0.0.0.0:9876 + volumeMounts: + - name: machine-controller-admission-cert + mountPath: /tmp/cert + livenessProbe: + httpGet: + path: /healthz + port: 9876 + initialDelaySeconds: 5 + periodSeconds: 5 + readinessProbe: + httpGet: + path: /healthz + port: 9876 + periodSeconds: 5 volumes: - name: machine-controller-admission-cert secret: @@ -216,7 +244,7 @@ data: apiVersion: v1 kind: Service metadata: - name: machine-controller + name: machine-controller-webhook namespace: kube-system spec: ports: @@ -225,7 +253,7 @@ spec: protocol: TCP targetPort: 9876 selector: - app: machine-controller + app: machine-controller-webhook type: ClusterIP --- apiVersion: v1 diff --git a/pkg/admission/admission.go b/pkg/admission/admission.go index 09654c7af..2b350f0ac 100644 --- a/pkg/admission/admission.go +++ b/pkg/admission/admission.go @@ -16,6 +16,7 @@ import ( func New(listenAddress string) *http.Server { m := http.NewServeMux() m.HandleFunc("/machinedeployments", handleFuncFactory(mutateMachineDeployments)) + m.HandleFunc("/healthz", healthZHandler) return &http.Server{ Addr: listenAddress, Handler: m, @@ -24,6 +25,10 @@ func New(listenAddress string) *http.Server { } } +func healthZHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} + func newJSONPatch(original, current runtime.Object) ([]jsonpatch.JsonPatchOperation, error) { originalGVK := original.GetObjectKind().GroupVersionKind() currentGVK := current.GetObjectKind().GroupVersionKind() diff --git a/pkg/admission/machinedeployments.go b/pkg/admission/machinedeployments.go index 6e2cc9a27..470aa8b69 100644 --- a/pkg/admission/machinedeployments.go +++ b/pkg/admission/machinedeployments.go @@ -69,6 +69,7 @@ func handleFuncFactory(mutate func(admissionv1beta1.AdmissionReview) (*admission contentType := r.Header.Get("Content-Type") if contentType != "application/json" { glog.Errorf("contentType=%s, expect application/json", contentType) + w.WriteHeader(http.StatusBadRequest) return } From 505a06a1b7f68d0ebe0a7a7307fa4abe06afd609 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Fri, 19 Oct 2018 15:14:53 +0200 Subject: [PATCH 16/23] Fix name of service --- Makefile | 2 +- examples/machine-controller.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 8f86f0c4f..1f19dc81f 100644 --- a/Makefile +++ b/Makefile @@ -101,7 +101,7 @@ examples/admission-key.pem: examples/ca-cert.pem examples/admission-cert.pem: examples/admission-key.pem openssl req -new -sha256 \ -key examples/admission-key.pem \ - -subj "/C=US/ST=CA/O=Acme/CN=machine-controller.kube-system.svc" \ + -subj "/C=US/ST=CA/O=Acme/CN=machine-controller-webhook.kube-system.svc" \ -out examples/admission.csr openssl x509 -req -in examples/admission.csr -CA examples/ca-cert.pem \ -CAkey examples/ca-key.pem -CAcreateserial \ diff --git a/examples/machine-controller.yaml b/examples/machine-controller.yaml index 55e5770f9..c34246061 100644 --- a/examples/machine-controller.yaml +++ b/examples/machine-controller.yaml @@ -492,6 +492,6 @@ webhooks: clientConfig: service: namespace: kube-system - name: machine-controller + name: machine-controller-webhook path: /machinedeployments caBundle: __admission_ca_cert__ From 181ae6167b39325295ae21e915152e96ffd35403 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Fri, 19 Oct 2018 16:00:26 +0200 Subject: [PATCH 17/23] Block webhook shutdown --- cmd/webhook/main.go | 12 ++++++++++-- examples/machine-controller.yaml | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index 77265be0c..7760ae0fb 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -21,6 +21,14 @@ func main() { flag.Parse() s := admission.New(admissionListenAddress) - glog.Infof("Starting to listen on %s", admissionListenAddress) - glog.Fatal(s.ListenAndServeTLS(admissionTLSCertPath, admissionTLSKeyPath)) + if err := s.ListenAndServeTLS(admissionTLSCertPath, admissionTLSKeyPath); err != nil { + glog.Fatalf("Failed to start server: %v", err) + } + defer func() { + if err := s.Close(); err != nil { + glog.Fatalf("Failed to shutdown server: %v", err) + } + }() + glog.Infof("Listening on %s", admissionListenAddress) + select {} } diff --git a/examples/machine-controller.yaml b/examples/machine-controller.yaml index c34246061..03c508dfc 100644 --- a/examples/machine-controller.yaml +++ b/examples/machine-controller.yaml @@ -220,12 +220,14 @@ spec: httpGet: path: /healthz port: 9876 + scheme: HTTPS initialDelaySeconds: 5 periodSeconds: 5 readinessProbe: httpGet: path: /healthz port: 9876 + scheme: HTTPS periodSeconds: 5 volumes: - name: machine-controller-admission-cert From a44c132059fa12da562609e89b920b19856783cf Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Fri, 19 Oct 2018 16:22:51 +0200 Subject: [PATCH 18/23] Use one replica for the webhook --- examples/machine-controller.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/machine-controller.yaml b/examples/machine-controller.yaml index 03c508dfc..abe799f77 100644 --- a/examples/machine-controller.yaml +++ b/examples/machine-controller.yaml @@ -195,7 +195,7 @@ metadata: name: machine-controller-webhook namespace: kube-system spec: - replicas: 2 + replicas: 1 selector: matchLabels: app: machine-controller-webhook From 2734863331da3929bc1153a512d54e0c873b480a Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Fri, 19 Oct 2018 18:59:36 +0200 Subject: [PATCH 19/23] Small fixes --- Makefile | 12 ++++-------- README.md | 2 +- test/tools/integration/provision_master.sh | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 1f19dc81f..2942bb525 100644 --- a/Makefile +++ b/Makefile @@ -46,21 +46,17 @@ docker-image: machine-controller docker-image-nodep admission-webhook docker-image-nodep: docker build -t $(IMAGE_NAME) . docker push $(IMAGE_NAME) - if [[ -n "$(GIT_TAG)" ]]; then \ - $(eval IMAGE_TAG = $(GIT_TAG)) \ - docker build -t $(IMAGE_NAME) . && \ - docker push $(IMAGE_NAME) && \ - $(eval IMAGE_TAG = latest) \ - docker build -t $(IMAGE_NAME) . ;\ - docker push $(IMAGE_NAME) ;\ - fi docker build -t $(IMAGE_NAME_WEBHOOK) -f Dockerfile.webhook . docker push $(IMAGE_NAME_WEBHOOK) if [[ -n "$(GIT_TAG)" ]]; then \ $(eval IMAGE_TAG = $(GIT_TAG)) \ + docker build -t $(IMAGE_NAME) . && \ + docker push $(IMAGE_NAME) && \ docker build -t $(IMAGE_NAME_WEBHOOK) -f Dockerfile.webhook . && \ docker push $(IMAGE_NAME_WEBHOOK) && \ $(eval IMAGE_TAG = latest) \ + docker build -t $(IMAGE_NAME) . ;\ + docker push $(IMAGE_NAME) ;\ docker build -t $(IMAGE_NAME_WEBHOOK) -f Dockerfile.webhook . ;\ docker push $(IMAGE_NAME_WEBHOOK) ;\ fi diff --git a/README.md b/README.md index ee9badf8d..8ceb4dd7e 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ ## Deploy the machine-controller -`kubectl apply -f examples/machine-controller.yaml` +`make deploy` ## Creating a machineDeployment ```bash diff --git a/test/tools/integration/provision_master.sh b/test/tools/integration/provision_master.sh index 816966aee..5a8224ce4 100755 --- a/test/tools/integration/provision_master.sh +++ b/test/tools/integration/provision_master.sh @@ -91,7 +91,7 @@ done echo "Error: machine-controller didn't come up within 100 seconds!" echo "Logs:" -kubectl logs -n kube-system -c machine-controller \$(kubectl get pods -n kube-system|egrep '^machine-controller'|awk '{ print \$1}') +kubectl logs -n kube-system \$(kubectl get pods -n kube-system|egrep '^machine-controller'|awk '{ print \$1}') exit 1 EOEXEC From b569ac6e741be237fc33af084689bd8dfb206f9b Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Fri, 19 Oct 2018 19:02:09 +0200 Subject: [PATCH 20/23] Clean up unneeded temporal changes --- cmd/controller/main.go | 4 ++-- test/tools/integration/hetzner.tf | 2 +- test/tools/integration/provision_master.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/controller/main.go b/cmd/controller/main.go index eb2782b58..965f5655d 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -260,11 +260,11 @@ func main() { g.Add(func() error { return s.ListenAndServe() }, func(err error) { - glog.Warningf("shutting down util HTTP server due to: %s", err) + glog.Warningf("shutting down HTTP server due to: %s", err) srvCtx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() if err = s.Shutdown(srvCtx); err != nil { - glog.Errorf("failed to shutdown util HTTP server: %s", err) + glog.Errorf("failed to shutdown HTTP server: %s", err) } }) } diff --git a/test/tools/integration/hetzner.tf b/test/tools/integration/hetzner.tf index 3f72bfa4a..3d6dd4d58 100644 --- a/test/tools/integration/hetzner.tf +++ b/test/tools/integration/hetzner.tf @@ -10,6 +10,6 @@ resource "hcloud_ssh_key" "default" { resource "hcloud_server" "machine-controller-test" { name = "${var.hcloud_test_server_name}" image = "ubuntu-18.04" - server_type = "cx51" + server_type = "cx41" ssh_keys = ["${hcloud_ssh_key.default.id}"] } diff --git a/test/tools/integration/provision_master.sh b/test/tools/integration/provision_master.sh index 5a8224ce4..2153ab85c 100755 --- a/test/tools/integration/provision_master.sh +++ b/test/tools/integration/provision_master.sh @@ -91,7 +91,7 @@ done echo "Error: machine-controller didn't come up within 100 seconds!" echo "Logs:" -kubectl logs -n kube-system \$(kubectl get pods -n kube-system|egrep '^machine-controller'|awk '{ print \$1}') +kubectl logs -n kube-system \$(kubectl get pods -n kube-system|egrep '^machine-controller'|awk '{ print \$1}') exit 1 EOEXEC From 62c75550a9c0b68ddf77c0a0cdb70c83f8b0f754 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Fri, 19 Oct 2018 19:04:55 +0200 Subject: [PATCH 21/23] Fix docker-image target --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2942bb525..a59c1fcef 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,7 @@ webhook: $(shell find cmd pkg -name '*.go') vendor -o webhook \ github.com/kubermatic/machine-controller/cmd/webhook -docker-image: machine-controller docker-image-nodep admission-webhook +docker-image: machine-controller admission-webhook docker-image-nodep # This target exists because in our CI # we do not want to restore the vendor From 1af777f774a21637a9f8592ebd9fcb90e95b1ec4 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Fri, 19 Oct 2018 19:06:56 +0200 Subject: [PATCH 22/23] Fix test if machine-controller is running --- test/tools/integration/provision_master.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tools/integration/provision_master.sh b/test/tools/integration/provision_master.sh index 2153ab85c..7f16766b0 100755 --- a/test/tools/integration/provision_master.sh +++ b/test/tools/integration/provision_master.sh @@ -82,7 +82,7 @@ if ! ls machine-controller-deployed; then fi for try in {1..10}; do - if kubectl get pods -n kube-system|egrep '^machine-controller'|grep Running; then + if kubectl get pods -n kube-system|egrep '^machine-controller'|grep -v webhook|grep Running; then echo "Success!" exit 0 fi From b981212b7ee04f2fbedd8034f9e911765f963b32 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Fri, 19 Oct 2018 20:09:56 +0200 Subject: [PATCH 23/23] Use one docker image for everything --- Dockerfile | 1 + Dockerfile.webhook | 5 ----- Makefile | 7 ------- examples/machine-controller.yaml | 2 +- test/tools/integration/provision_master.sh | 3 +-- 5 files changed, 3 insertions(+), 15 deletions(-) delete mode 100644 Dockerfile.webhook diff --git a/Dockerfile b/Dockerfile index 000754e69..eab1fca76 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,5 +3,6 @@ FROM alpine:3.7 RUN apk add --no-cache ca-certificates cdrkit COPY machine-controller /usr/local/bin +COPY webhook /usr/local/bin USER nobody diff --git a/Dockerfile.webhook b/Dockerfile.webhook deleted file mode 100644 index 57e131c52..000000000 --- a/Dockerfile.webhook +++ /dev/null @@ -1,5 +0,0 @@ -FROM alpine:3.7 - -COPY webhook /usr/local/bin - -USER nobody diff --git a/Makefile b/Makefile index a59c1fcef..a847b089c 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,6 @@ REGISTRY_NAMESPACE ?= kubermatic IMAGE_TAG = \ $(shell echo $$(git rev-parse HEAD && if [[ -n $$(git status --porcelain) ]]; then echo '-dirty'; fi)|tr -d ' ') IMAGE_NAME = $(REGISTRY)/$(REGISTRY_NAMESPACE)/machine-controller:$(IMAGE_TAG) -IMAGE_NAME_WEBHOOK = $(REGISTRY)/$(REGISTRY_NAMESPACE)/machine-controller-webhook:$(IMAGE_TAG) vendor: Gopkg.lock Gopkg.toml @@ -46,19 +45,13 @@ docker-image: machine-controller admission-webhook docker-image-nodep docker-image-nodep: docker build -t $(IMAGE_NAME) . docker push $(IMAGE_NAME) - docker build -t $(IMAGE_NAME_WEBHOOK) -f Dockerfile.webhook . - docker push $(IMAGE_NAME_WEBHOOK) if [[ -n "$(GIT_TAG)" ]]; then \ $(eval IMAGE_TAG = $(GIT_TAG)) \ docker build -t $(IMAGE_NAME) . && \ docker push $(IMAGE_NAME) && \ - docker build -t $(IMAGE_NAME_WEBHOOK) -f Dockerfile.webhook . && \ - docker push $(IMAGE_NAME_WEBHOOK) && \ $(eval IMAGE_TAG = latest) \ docker build -t $(IMAGE_NAME) . ;\ docker push $(IMAGE_NAME) ;\ - docker build -t $(IMAGE_NAME_WEBHOOK) -f Dockerfile.webhook . ;\ - docker push $(IMAGE_NAME_WEBHOOK) ;\ fi test-unit-docker: diff --git a/examples/machine-controller.yaml b/examples/machine-controller.yaml index abe799f77..b9bdc8ad6 100644 --- a/examples/machine-controller.yaml +++ b/examples/machine-controller.yaml @@ -205,7 +205,7 @@ spec: app: machine-controller-webhook spec: containers: - - image: kubermatic/machine-controller-webhook:latest + - image: kubermatic/machine-controller:latest imagePullPolicy: IfNotPresent name: webhook command: diff --git a/test/tools/integration/provision_master.sh b/test/tools/integration/provision_master.sh index 7f16766b0..56e77ece4 100755 --- a/test/tools/integration/provision_master.sh +++ b/test/tools/integration/provision_master.sh @@ -16,7 +16,7 @@ for try in {1..100}; do done rsync -avR -e "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" \ - ../../.././{Makefile,examples/machine-controller.yaml,machine-controller,Dockerfile,webhook,Dockerfile.webhook} \ + ../../.././{Makefile,examples/machine-controller.yaml,machine-controller,Dockerfile,webhook} \ root@$ADDR:/root/ cat <