From a40525b382773c463ac1e4d05cb346fc2b938367 Mon Sep 17 00:00:00 2001 From: Abhinav Dahiya Date: Tue, 18 Sep 2018 19:03:25 -0700 Subject: [PATCH 1/5] *: update install assets --- Dockerfile | 7 ++- Gopkg.lock | 3 +- hack/build-image.sh | 25 ++++++++++ ...0_clusterversionoperator_00_namespace.yaml | 6 +++ ...usterversionoperator_01_cvoconfig.crd.yaml | 24 ++++++++++ .../00_clusterversionoperator_02_roles.yaml | 11 +++++ install/00_clusterversionoperator_03_scc.yaml | 30 ++++++++++++ ..._clusterversionoperator_04_deployment.yaml | 48 +++++++++++++++++++ 8 files changed, 152 insertions(+), 2 deletions(-) create mode 100755 hack/build-image.sh create mode 100644 install/00_clusterversionoperator_00_namespace.yaml create mode 100644 install/00_clusterversionoperator_01_cvoconfig.crd.yaml create mode 100644 install/00_clusterversionoperator_02_roles.yaml create mode 100644 install/00_clusterversionoperator_03_scc.yaml create mode 100644 install/00_clusterversionoperator_04_deployment.yaml diff --git a/Dockerfile b/Dockerfile index 36dc6b478..677246cd0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,12 @@ COPY . /go/src/github.com/openshift/cluster-version-operator WORKDIR /go/src/github.com/openshift/cluster-version-operator RUN ./hack/build-go.sh -FROM scratch +# Using alpine instead of scratch because the Job +# used to extract updatepayload from CVO image uses +# cp command. +FROM alpine COPY --from=build-env /go/src/github.com/openshift/cluster-version-operator/_output/linux/amd64/cluster-version-operator /bin/cluster-version-operator +COPY install /manifests ENTRYPOINT ["/bin/cluster-version-operator"] +LABEL io.openshift.release.operator true \ No newline at end of file diff --git a/Gopkg.lock b/Gopkg.lock index 9a19be551..67b1d4744 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -593,6 +593,7 @@ "k8s.io/api/batch/v1", "k8s.io/api/core/v1", "k8s.io/api/rbac/v1", + "k8s.io/api/rbac/v1beta1", "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1", "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset", "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1", @@ -609,9 +610,9 @@ "k8s.io/apimachinery/pkg/runtime/serializer", "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/wait", "k8s.io/apimachinery/pkg/util/yaml", "k8s.io/apimachinery/pkg/watch", diff --git a/hack/build-image.sh b/hack/build-image.sh new file mode 100755 index 000000000..ee8fa01d1 --- /dev/null +++ b/hack/build-image.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -eu + +# Print errors to stderr +function print_error { + echo "ERROR: $1" >&2 +} + +function print_info { + echo "INFO: $1" >&2 +} + +# Warn when unprivileged +if [ `id --user` -ne 0 ]; then + print_error "Note: Building unprivileged may fail due to permissions" +fi + +if [ -z ${VERSION+a} ]; then + print_info "Using version from git..." + VERSION=$(git describe --abbrev=8 --dirty --always) +fi + +set -x +podman build -t "cluster-version-operator:${VERSION}" -f Dockerfile \ No newline at end of file diff --git a/install/00_clusterversionoperator_00_namespace.yaml b/install/00_clusterversionoperator_00_namespace.yaml new file mode 100644 index 000000000..060d621e8 --- /dev/null +++ b/install/00_clusterversionoperator_00_namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: openshift-cluster-version + labels: + name: openshift-cluster-version diff --git a/install/00_clusterversionoperator_01_cvoconfig.crd.yaml b/install/00_clusterversionoperator_01_cvoconfig.crd.yaml new file mode 100644 index 000000000..8a7bff6aa --- /dev/null +++ b/install/00_clusterversionoperator_01_cvoconfig.crd.yaml @@ -0,0 +1,24 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + # name must match the spec fields below, and be in the form: . + name: cvoconfigs.clusterversion.openshift.io +spec: + # group name to use for REST API: /apis// + group: clusterversion.openshift.io + # list of versions supported by this CustomResourceDefinition + versions: + - name: v1 + # Each version can be enabled/disabled by Served flag. + served: true + # One and only one version must be marked as the storage version. + storage: true + # either Namespaced or Cluster + scope: Namespaced + names: + # plural name to be used in the URL: /apis/// + plural: cvoconfigs + # singular name to be used as an alias on the CLI and for display + singular: cvoconfig + # kind is normally the CamelCased singular type. Your resource manifests use this. + kind: CVOConfig diff --git a/install/00_clusterversionoperator_02_roles.yaml b/install/00_clusterversionoperator_02_roles.yaml new file mode 100644 index 000000000..d5c743678 --- /dev/null +++ b/install/00_clusterversionoperator_02_roles.yaml @@ -0,0 +1,11 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: cluster-version-operator +roleRef: + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + namespace: openshift-cluster-version + name: default diff --git a/install/00_clusterversionoperator_03_scc.yaml b/install/00_clusterversionoperator_03_scc.yaml new file mode 100644 index 000000000..289b45399 --- /dev/null +++ b/install/00_clusterversionoperator_03_scc.yaml @@ -0,0 +1,30 @@ +apiVersion: security.openshift.io/v1 +kind: SecurityContextConstraints +metadata: + annotations: + kubernetes.io/description: "privileged-cluster-version-operator for running cluster version operator." + name: privileged-cluster-version-operator +allowHostDirVolumePlugin: true +allowHostIPC: true +allowHostNetwork: true +allowHostPID: true +allowHostPorts: true +allowPrivilegedContainer: true +allowedCapabilities: +- "*" +fsGroup: + type: RunAsAny +groups: +- system:serviceaccounts:openshift-cluster-version +readOnlyRootFilesystem: false +runAsUser: + type: RunAsAny +seLinuxContext: + type: RunAsAny +seccompProfiles: +- "*" +supplementalGroups: + type: RunAsAny +users: [] +volumes: +- "*" \ No newline at end of file diff --git a/install/00_clusterversionoperator_04_deployment.yaml b/install/00_clusterversionoperator_04_deployment.yaml new file mode 100644 index 000000000..293929c65 --- /dev/null +++ b/install/00_clusterversionoperator_04_deployment.yaml @@ -0,0 +1,48 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cluster-version-operator + namespace: openshift-cluster-version +spec: + selector: + matchLabels: + k8s-app: cluster-version-operator + template: + metadata: + name: cluster-version-operator + labels: + k8s-app: cluster-version-operator + spec: + containers: + - name: cluster-version-operator + image: docker.io/origin/origin-cluster-version-operator:v4.0.0 + imagePullPolicy: Always + args: + - "start" + - "--enable-auto-update=false" + - "--v=4" + volumeMounts: + - mountPath: /etc/ssl/certs + name: etc-ssl-certs + readOnly: true + - mountPath: /etc/cvo/updatepayloads + name: etc-cvo-updatepayloads + readOnly: true + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + nodeSelector: + node-role.kubernetes.io/master: "" + tolerations: + - key: node-role.kubernetes.io/master + operator: Exists + effect: NoSchedule + volumes: + - name: etc-ssl-certs + hostPath: + path: /etc/ssl/certs + - name: etc-cvo-updatepayloads + hostPath: + path: /etc/cvo/updatepayloads From 3e203bec1bc66dc1614fce51e6ed071ba33b7c2d Mon Sep 17 00:00:00 2001 From: Abhinav Dahiya Date: Tue, 18 Sep 2018 19:03:57 -0700 Subject: [PATCH 2/5] lib: update manifest to read from files --- lib/manifest.go | 105 ++-------------- lib/manifest_test.go | 293 ++++--------------------------------------- 2 files changed, 34 insertions(+), 364 deletions(-) diff --git a/lib/manifest.go b/lib/manifest.go index cf12c864d..d48d11d39 100644 --- a/lib/manifest.go +++ b/lib/manifest.go @@ -5,10 +5,7 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" - "path/filepath" - "sort" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -61,108 +58,22 @@ func (m *Manifest) UnmarshalJSON(in []byte) error { // Object returns underlying metav1.Object func (m *Manifest) Object() metav1.Object { return m.obj } -const ( - // rootDirKey is used as key for the manifest files in root dir - // passed to LoadManifests - // It is set to `000` to give it more priority if the actor sorts - // based on keys. - rootDirKey = "000" -) - -// LoadManifests loads manifest from disk. -// -// root/ -// manifest0 -// manifest1 -// 00_subdir0/ -// manifest0 -// manifest1 -// 01_subdir1/ -// manifest0 -// manifest1 -// LoadManifests(/root): -// returns map -// 000: [manifest0, manifest1] -// 00_subdir0: [manifest0, manifest1] -// 01_subdir1: [manifest0, manifest1] -// -// It skips dirs that have not files. -// It only reads dir `p` and its direct subdirs. -func LoadManifests(p string) (map[string][]Manifest, error) { - var out = make(map[string][]Manifest) - - fs, err := ioutil.ReadDir(p) - if err != nil { - return nil, err - } - - // We want to accumulate all the errors, not returning at the - // first error encountered when reading subdirs. - var errs []error - - // load manifest files in p to rootDirKey - ms, err := loadManifestsFromDir(p) - if err != nil { - errs = append(errs, fmt.Errorf("error loading from dir %s: %v", p, err)) - } - if len(ms) > 0 { - out[rootDirKey] = ms - } - - // load manifests from subdirs to subdir-name - for _, f := range fs { - if !f.IsDir() { - continue - } - path := filepath.Join(p, f.Name()) - ms, err := loadManifestsFromDir(path) - if err != nil { - errs = append(errs, fmt.Errorf("error loading from dir %s: %v", path, err)) - continue - } - if len(ms) > 0 { - out[f.Name()] = ms - } - } - - agg := utilerrors.NewAggregate(errs) - if agg != nil { - return nil, errors.New(agg.Error()) - } - return out, nil -} - -// loadManifestsFromDir only returns files. not subdirs are traversed. -// returns manifests in increasing order of their filename. -func loadManifestsFromDir(dir string) ([]Manifest, error) { +// ManifestsFromFiles reads files and returns Manifests in the same order. +// files should be list of absolute paths for the manifests on disk. +func ManifestsFromFiles(files []string) ([]Manifest, error) { var manifests []Manifest - fs, err := ioutil.ReadDir(dir) - if err != nil { - return nil, err - } - - // ensure sorted. - sort.Slice(fs, func(i, j int) bool { - return fs[i].Name() < fs[j].Name() - }) - var errs []error - for _, f := range fs { - if f.IsDir() { - continue - } - - path := filepath.Join(dir, f.Name()) - file, err := os.Open(path) + for _, file := range files { + file, err := os.Open(file) if err != nil { - errs = append(errs, fmt.Errorf("error opening %s: %v", path, err)) + errs = append(errs, fmt.Errorf("error opening %s: %v", file.Name(), err)) continue } defer file.Close() ms, err := parseManifests(file) if err != nil { - errs = append(errs, fmt.Errorf("error parsing %s: %v", path, err)) + errs = append(errs, fmt.Errorf("error parsing %s: %v", file.Name(), err)) continue } manifests = append(manifests, ms...) @@ -170,7 +81,7 @@ func loadManifestsFromDir(dir string) ([]Manifest, error) { agg := utilerrors.NewAggregate(errs) if agg != nil { - return nil, fmt.Errorf("error loading manifests from %q: %v", dir, agg.Error()) + return nil, fmt.Errorf("error loading manifests: %v", agg.Error()) } return manifests, nil diff --git a/lib/manifest_test.go b/lib/manifest_test.go index 1cb22f631..90f32c015 100644 --- a/lib/manifest_test.go +++ b/lib/manifest_test.go @@ -5,7 +5,6 @@ import ( "os" "path/filepath" "reflect" - "sort" "strings" "testing" @@ -150,24 +149,20 @@ data: } -func TestLoadManifestsFromDir(t *testing.T) { +func TestManifestsFromFiles(t *testing.T) { tests := []struct { name string - fs []dir + fs dir want []Manifest }{{ name: "no-files", - fs: []dir{{ + fs: dir{ name: "a", - }, { - name: "a/00_dir", - }, { - name: "a/01_dir", - }}, + }, want: nil, }, { name: "all-files", - fs: []dir{{ + fs: dir{ name: "a", files: []file{{ name: "f0", @@ -201,15 +196,15 @@ data: how are you? `, }}, - }}, + }, want: []Manifest{{ GVK: schema.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Ingress"}, }, { GVK: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"}, }}, }, { - name: "files-and-subdirs", - fs: []dir{{ + name: "files-with-multiple-manifests", + fs: dir{ name: "a", files: []file{{ name: "f0", @@ -227,10 +222,7 @@ spec: backend: serviceName: test servicePort: 80 -`, - }, { - name: "f1", - contents: ` +--- apiVersion: v1 kind: ConfigMap metadata: @@ -241,26 +233,6 @@ data: multi-line: | hello world how are you? -`, - }}, - }, { - name: "a/00_dir", - files: []file{{ - name: "f0", - contents: ` -apiVersion: extensions/v1beta1 -kind: Ingress -metadata: - name: test-ingress - namespace: test-namespace -spec: - rules: - - http: - paths: - - path: /testpath - backend: - serviceName: test - servicePort: 80 `, }, { name: "f1", @@ -277,45 +249,13 @@ data: how are you? `, }}, - }, { - name: "a/01_dir", - files: []file{{ - name: "f0", - contents: ` -apiVersion: extensions/v1beta1 -kind: Ingress -metadata: - name: test-ingress - namespace: test-namespace -spec: - rules: - - http: - paths: - - path: /testpath - backend: - serviceName: test - servicePort: 80 -`, - }, { - name: "f1", - contents: ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: a-config - namespace: default -data: - color: "red" - multi-line: | - hello world - how are you? -`, - }}, - }}, + }, want: []Manifest{{ GVK: schema.GroupVersionKind{Group: "extensions", Version: "v1beta1", Kind: "Ingress"}, }, { GVK: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"}, + }, { + GVK: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"}, }}, }} for _, test := range tests { @@ -327,7 +267,11 @@ data: } }() - got, err := loadManifestsFromDir(filepath.Join(tmpdir, "a")) + files := []string{} + for _, f := range test.fs.files { + files = append(files, filepath.Join(tmpdir, test.fs.name, f.name)) + } + got, err := ManifestsFromFiles(files) if err != nil { t.Fatal(err) } @@ -342,189 +286,6 @@ data: } } -func TestLoadManifests(t *testing.T) { - tests := []struct { - name string - fs []dir - - wantkeys []string - wantlen []int - }{{ - name: "empty-root", - fs: []dir{{ - name: "a", - }, { - name: "a/00_dir", - files: []file{{ - name: "f0", - contents: ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: a-config - namespace: default -data: - color: "red" - multi-line: | - hello world - how are you? -`, - }}, - }, { - name: "a/01_dir", - files: []file{{ - name: "f0", - contents: ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: a-config - namespace: default -data: - color: "red" - multi-line: | - hello world - how are you? -`, - }}, - }}, - wantkeys: []string{"00_dir", "01_dir"}, - wantlen: []int{1, 1}, - }, { - name: "non-empty-root", - fs: []dir{{ - name: "a", - files: []file{{ - name: "f0", - contents: ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: a-config - namespace: default -data: - color: "red" - multi-line: | - hello world - how are you? -`, - }}, - }, { - name: "a/00_dir", - files: []file{{ - name: "f0", - contents: ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: a-config - namespace: default -data: - color: "red" - multi-line: | - hello world - how are you? -`, - }}, - }, { - name: "a/01_dir", - files: []file{{ - name: "f0", - contents: ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: a-config - namespace: default -data: - color: "red" - multi-line: | - hello world - how are you? -`, - }}, - }}, - wantkeys: []string{"000", "00_dir", "01_dir"}, - wantlen: []int{1, 1, 1}, - }, { - name: "empty-sub-dir", - fs: []dir{{ - name: "a", - files: []file{{ - name: "f0", - contents: ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: a-config - namespace: default -data: - color: "red" - multi-line: | - hello world - how are you? -`, - }}, - }, { - name: "a/00_dir", - files: []file{{ - name: "f0", - contents: ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: a-config - namespace: default -data: - color: "red" - multi-line: | - hello world - how are you? -`, - }}, - }, { - name: "a/01_dir", - }}, - wantkeys: []string{"000", "00_dir"}, - wantlen: []int{1, 1}, - }} - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - tmpdir, cleanup := setupTestFS(t, test.fs) - defer func() { - if err := cleanup(); err != nil { - t.Logf("error cleaning %q", tmpdir) - } - }() - - got, err := LoadManifests(filepath.Join(tmpdir, "a")) - if err != nil { - t.Fatal(err) - } - - gotkeys := []string{} - for k := range got { - gotkeys = append(gotkeys, k) - for j := range got[k] { - (got[k])[j].Raw = nil - } - } - sort.Strings(gotkeys) - gotlen := []int{} - for _, k := range gotkeys { - gotlen = append(gotlen, len(got[k])) - } - if !reflect.DeepEqual(test.wantkeys, gotkeys) { - t.Fatalf("mismatch \ngot: %s \nwant: %s", spew.Sdump(gotkeys), spew.Sdump(test.wantkeys)) - } - if !reflect.DeepEqual(test.wantlen, gotlen) { - t.Fatalf("mismatch \ngot: %s \nwant: %s", spew.Sdump(gotlen), spew.Sdump(test.wantlen)) - } - }) - } -} - type file struct { name string contents string @@ -535,21 +296,19 @@ type dir struct { files []file } -// setupTestFS returns path of the tmp dir created and cleanup function. -func setupTestFS(t *testing.T, dirs []dir) (string, func() error) { +// setupTestFS returns path of the tmp d created and cleanup function. +func setupTestFS(t *testing.T, d dir) (string, func() error) { root, err := ioutil.TempDir("", "test") if err != nil { t.Fatal(err) } - for _, dir := range dirs { - dpath := filepath.Join(root, dir.name) - if err := os.MkdirAll(dpath, 0755); err != nil { - t.Fatal(err) - } - for _, file := range dir.files { - path := filepath.Join(dpath, file.name) - ioutil.WriteFile(path, []byte(file.contents), 0755) - } + dpath := filepath.Join(root, d.name) + if err := os.MkdirAll(dpath, 0755); err != nil { + t.Fatal(err) + } + for _, file := range d.files { + path := filepath.Join(dpath, file.name) + ioutil.WriteFile(path, []byte(file.contents), 0755) } cleanup := func() error { return os.RemoveAll(root) From 97a31863b0a0292fb87021580954fa52282d16e7 Mon Sep 17 00:00:00 2001 From: Abhinav Dahiya Date: Tue, 18 Sep 2018 19:04:21 -0700 Subject: [PATCH 3/5] lib: generic resource builder --- lib/resourceapply/core.go | 21 ++++++ lib/resourceapply/rbac.go | 42 ++++++++++++ lib/resourcebuilder/apiext.go | 36 ++++------ lib/resourcebuilder/apps.go | 35 ++++------ lib/resourcebuilder/batch.go | 34 ++++------ lib/resourcebuilder/core.go | 85 ++++++++++++++--------- lib/resourcebuilder/interface.go | 77 +++++++++++++++------ lib/resourcebuilder/rbac.go | 112 ++++++++++++++++++++++--------- lib/resourcebuilder/register.go | 30 +++++++++ lib/resourcemerge/cv.go | 26 ------- lib/resourcemerge/os.go | 30 +++++++++ lib/resourcemerge/rbac.go | 24 +++++++ lib/resourceread/core.go | 8 +++ lib/resourceread/rbac.go | 22 ++++++ 14 files changed, 404 insertions(+), 178 deletions(-) create mode 100644 lib/resourcebuilder/register.go create mode 100644 lib/resourcemerge/os.go diff --git a/lib/resourceapply/core.go b/lib/resourceapply/core.go index eb8edab3d..68010962f 100644 --- a/lib/resourceapply/core.go +++ b/lib/resourceapply/core.go @@ -9,6 +9,27 @@ import ( "k8s.io/utils/pointer" ) +// ApplyNamespace merges objectmeta, does not worry about anything else +func ApplyNamespace(client coreclientv1.NamespacesGetter, required *corev1.Namespace) (*corev1.Namespace, bool, error) { + existing, err := client.Namespaces().Get(required.Name, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + actual, err := client.Namespaces().Create(required) + return actual, true, err + } + if err != nil { + return nil, false, err + } + + modified := pointer.BoolPtr(false) + resourcemerge.EnsureObjectMeta(modified, &existing.ObjectMeta, required.ObjectMeta) + if !*modified { + return existing, false, nil + } + + actual, err := client.Namespaces().Update(existing) + return actual, true, err +} + // ApplyServiceAccount applies the required serviceaccount to the cluster. func ApplyServiceAccount(client coreclientv1.ServiceAccountsGetter, required *corev1.ServiceAccount) (*corev1.ServiceAccount, bool, error) { existing, err := client.ServiceAccounts(required.Namespace).Get(required.Name, metav1.GetOptions{}) diff --git a/lib/resourceapply/rbac.go b/lib/resourceapply/rbac.go index a5f46bdd3..26a534e12 100644 --- a/lib/resourceapply/rbac.go +++ b/lib/resourceapply/rbac.go @@ -50,3 +50,45 @@ func ApplyClusterRole(client rbacclientv1.ClusterRolesGetter, required *rbacv1.C actual, err := client.ClusterRoles().Update(existing) return actual, true, err } + +// ApplyRoleBinding applies the required clusterrolebinding to the cluster. +func ApplyRoleBinding(client rbacclientv1.RoleBindingsGetter, required *rbacv1.RoleBinding) (*rbacv1.RoleBinding, bool, error) { + existing, err := client.RoleBindings(required.Namespace).Get(required.Name, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + actual, err := client.RoleBindings(required.Namespace).Create(required) + return actual, true, err + } + if err != nil { + return nil, false, err + } + + modified := pointer.BoolPtr(false) + resourcemerge.EnsureRoleBinding(modified, existing, *required) + if !*modified { + return existing, false, nil + } + + actual, err := client.RoleBindings(required.Namespace).Update(existing) + return actual, true, err +} + +// ApplyRole applies the required clusterrole to the cluster. +func ApplyRole(client rbacclientv1.RolesGetter, required *rbacv1.Role) (*rbacv1.Role, bool, error) { + existing, err := client.Roles(required.Namespace).Get(required.Name, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + actual, err := client.Roles(required.Namespace).Create(required) + return actual, true, err + } + if err != nil { + return nil, false, err + } + + modified := pointer.BoolPtr(false) + resourcemerge.EnsureRole(modified, existing, *required) + if !*modified { + return existing, false, nil + } + + actual, err := client.Roles(required.Namespace).Update(existing) + return actual, true, err +} diff --git a/lib/resourcebuilder/apiext.go b/lib/resourcebuilder/apiext.go index 7ff6ae70b..c25646bb4 100644 --- a/lib/resourcebuilder/apiext.go +++ b/lib/resourcebuilder/apiext.go @@ -1,7 +1,6 @@ package resourcebuilder import ( - "fmt" "time" "github.com/golang/glog" @@ -17,38 +16,29 @@ import ( "k8s.io/client-go/rest" ) -func newAPIExtBuilder(config *rest.Config, m lib.Manifest) (Interface, error) { - kind := m.GVK.Kind - switch kind { - case CRDKind: - return newCRDBuilder(config, m), nil - default: - return nil, fmt.Errorf("no APIExt builder found for %s", kind) - } -} - -const ( - // CRDKind is kind for CustomResourceDefinitions - CRDKind = "CustomResourceDefinition" -) - type crdBuilder struct { - client *apiextclientv1beta1.ApiextensionsV1beta1Client - raw []byte + client *apiextclientv1beta1.ApiextensionsV1beta1Client + raw []byte + modifier MetaV1ObjectModifierFunc } -func newCRDBuilder(config *rest.Config, m lib.Manifest) *crdBuilder { +func newCRDBuilder(config *rest.Config, m lib.Manifest) Interface { return &crdBuilder{ client: apiextclientv1beta1.NewForConfigOrDie(config), raw: m.Raw, } } -func (b *crdBuilder) Do(modifier MetaV1ObjectModifierFunc) error { - crd := resourceread.ReadCustomResourceDefinitionV1Beta1OrDie(b.raw) - - modifier(crd) +func (b *crdBuilder) WithModifier(f MetaV1ObjectModifierFunc) Interface { + b.modifier = f + return b +} +func (b *crdBuilder) Do() error { + crd := resourceread.ReadCustomResourceDefinitionV1Beta1OrDie(b.raw) + if b.modifier != nil { + b.modifier(crd) + } _, updated, err := resourceapply.ApplyCustomResourceDefinition(b.client, crd) if err != nil { return err diff --git a/lib/resourcebuilder/apps.go b/lib/resourcebuilder/apps.go index fd9d0d8a5..df7d9ab75 100644 --- a/lib/resourcebuilder/apps.go +++ b/lib/resourcebuilder/apps.go @@ -17,38 +17,29 @@ import ( "k8s.io/client-go/rest" ) -func newAppsBuilder(config *rest.Config, m lib.Manifest) (Interface, error) { - kind := m.GVK.Kind - switch kind { - case DeploymentKind: - return newDeploymentBuilder(config, m), nil - default: - return nil, fmt.Errorf("no Apps builder found for %s", kind) - } -} - -const ( - // DeploymentKind is kind for Deployment. - DeploymentKind = "Deployment" -) - type deploymentBuilder struct { - client *appsclientv1.AppsV1Client - raw []byte + client *appsclientv1.AppsV1Client + raw []byte + modifier MetaV1ObjectModifierFunc } -func newDeploymentBuilder(config *rest.Config, m lib.Manifest) *deploymentBuilder { +func newDeploymentBuilder(config *rest.Config, m lib.Manifest) Interface { return &deploymentBuilder{ client: appsclientv1.NewForConfigOrDie(config), raw: m.Raw, } } -func (b *deploymentBuilder) Do(modifier MetaV1ObjectModifierFunc) error { - deployment := resourceread.ReadDeploymentV1OrDie(b.raw) - - modifier(deployment) +func (b *deploymentBuilder) WithModifier(f MetaV1ObjectModifierFunc) Interface { + b.modifier = f + return b +} +func (b *deploymentBuilder) Do() error { + deployment := resourceread.ReadDeploymentV1OrDie(b.raw) + if b.modifier != nil { + b.modifier(deployment) + } _, updated, err := resourceapply.ApplyDeployment(b.client, deployment) if err != nil { return err diff --git a/lib/resourcebuilder/batch.go b/lib/resourcebuilder/batch.go index 6e910344c..e57953e98 100644 --- a/lib/resourcebuilder/batch.go +++ b/lib/resourcebuilder/batch.go @@ -16,37 +16,29 @@ import ( "k8s.io/client-go/rest" ) -func newBatchBuilder(config *rest.Config, m lib.Manifest) (Interface, error) { - kind := m.GVK.Kind - switch kind { - case jobKind: - return newJobBuilder(config, m), nil - default: - return nil, fmt.Errorf("no Batch builder found for %s", kind) - } -} - -const ( - jobKind = "Job" -) - type jobBuilder struct { - client *batchclientv1.BatchV1Client - raw []byte + client *batchclientv1.BatchV1Client + raw []byte + modifier MetaV1ObjectModifierFunc } -func newJobBuilder(config *rest.Config, m lib.Manifest) *jobBuilder { +func newJobBuilder(config *rest.Config, m lib.Manifest) Interface { return &jobBuilder{ client: batchclientv1.NewForConfigOrDie(config), raw: m.Raw, } } -func (b *jobBuilder) Do(modifier MetaV1ObjectModifierFunc) error { - job := resourceread.ReadJobV1OrDie(b.raw) - - modifier(job) +func (b *jobBuilder) WithModifier(f MetaV1ObjectModifierFunc) Interface { + b.modifier = f + return b +} +func (b *jobBuilder) Do() error { + job := resourceread.ReadJobV1OrDie(b.raw) + if b.modifier != nil { + b.modifier(job) + } _, updated, err := resourceapply.ApplyJob(b.client, job) if err != nil { return err diff --git a/lib/resourcebuilder/core.go b/lib/resourcebuilder/core.go index 5579de8f7..f391ac7fb 100644 --- a/lib/resourcebuilder/core.go +++ b/lib/resourcebuilder/core.go @@ -1,8 +1,6 @@ package resourcebuilder import ( - "fmt" - "github.com/openshift/cluster-version-operator/lib" "github.com/openshift/cluster-version-operator/lib/resourceapply" "github.com/openshift/cluster-version-operator/lib/resourceread" @@ -10,60 +8,83 @@ import ( "k8s.io/client-go/rest" ) -func newCoreBuilder(config *rest.Config, m lib.Manifest) (Interface, error) { - kind := m.GVK.Kind - switch kind { - case serviceAccountKind: - return newServiceAccountBuilder(config, m), nil - case configMapKind: - return newConfigMapBuilder(config, m), nil - default: - return nil, fmt.Errorf("no Core builder found for %s", kind) - } -} - -const ( - serviceAccountKind = "ServiceAccount" -) - type serviceAccountBuilder struct { - client *coreclientv1.CoreV1Client - raw []byte + client *coreclientv1.CoreV1Client + raw []byte + modifier MetaV1ObjectModifierFunc } -func newServiceAccountBuilder(config *rest.Config, m lib.Manifest) *serviceAccountBuilder { +func newServiceAccountBuilder(config *rest.Config, m lib.Manifest) Interface { return &serviceAccountBuilder{ client: coreclientv1.NewForConfigOrDie(config), raw: m.Raw, } } -func (b *serviceAccountBuilder) Do(modifier MetaV1ObjectModifierFunc) error { +func (b *serviceAccountBuilder) WithModifier(f MetaV1ObjectModifierFunc) Interface { + b.modifier = f + return b +} + +func (b *serviceAccountBuilder) Do() error { serviceAccount := resourceread.ReadServiceAccountV1OrDie(b.raw) - modifier(serviceAccount) + if b.modifier != nil { + b.modifier(serviceAccount) + } _, _, err := resourceapply.ApplyServiceAccount(b.client, serviceAccount) return err } -const ( - configMapKind = "ConfigMap" -) - type configMapBuilder struct { - client *coreclientv1.CoreV1Client - raw []byte + client *coreclientv1.CoreV1Client + raw []byte + modifier MetaV1ObjectModifierFunc } -func newConfigMapBuilder(config *rest.Config, m lib.Manifest) *configMapBuilder { +func newConfigMapBuilder(config *rest.Config, m lib.Manifest) Interface { return &configMapBuilder{ client: coreclientv1.NewForConfigOrDie(config), raw: m.Raw, } } -func (b *configMapBuilder) Do(modifier MetaV1ObjectModifierFunc) error { +func (b *configMapBuilder) WithModifier(f MetaV1ObjectModifierFunc) Interface { + b.modifier = f + return b +} + +func (b *configMapBuilder) Do() error { configMap := resourceread.ReadConfigMapV1OrDie(b.raw) - modifier(configMap) + if b.modifier != nil { + b.modifier(configMap) + } _, _, err := resourceapply.ApplyConfigMap(b.client, configMap) return err } + +type namespaceBuilder struct { + client *coreclientv1.CoreV1Client + raw []byte + modifier MetaV1ObjectModifierFunc +} + +func newNamespaceBuilder(config *rest.Config, m lib.Manifest) Interface { + return &namespaceBuilder{ + client: coreclientv1.NewForConfigOrDie(config), + raw: m.Raw, + } +} + +func (b *namespaceBuilder) WithModifier(f MetaV1ObjectModifierFunc) Interface { + b.modifier = f + return b +} + +func (b *namespaceBuilder) Do() error { + namespace := resourceread.ReadNamespaceV1OrDie(b.raw) + if b.modifier != nil { + b.modifier(namespace) + } + _, _, err := resourceapply.ApplyNamespace(b.client, namespace) + return err +} diff --git a/lib/resourcebuilder/interface.go b/lib/resourcebuilder/interface.go index 942b4062f..1214056b9 100644 --- a/lib/resourcebuilder/interface.go +++ b/lib/resourcebuilder/interface.go @@ -2,37 +2,70 @@ package resourcebuilder import ( "fmt" + "sync" - "github.com/openshift/cluster-version-operator/lib" - appsv1 "k8s.io/api/apps/v1" - batchv1 "k8s.io/api/batch/v1" - corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/rest" + + "github.com/openshift/cluster-version-operator/lib" +) + +var ( + // Mapper is default ResourceMapper. + Mapper = NewResourceMapper() ) +// ResourceMapper maps {Group, Version} to a function that returns Interface and an error. +type ResourceMapper struct { + l *sync.Mutex + + gvkToNew map[schema.GroupVersionKind]NewInteraceFunc +} + +// AddToMap adds all keys from caller to input. +// Locks the input ResourceMapper before adding the keys from caller. +func (rm *ResourceMapper) AddToMap(irm *ResourceMapper) { + irm.l.Lock() + defer irm.l.Unlock() + for k, v := range rm.gvkToNew { + irm.gvkToNew[k] = v + } +} + +// RegisterGVK adds GVK to NewInteraceFunc mapping. +// It does not lock before adding the mapping. +func (rm *ResourceMapper) RegisterGVK(gvk schema.GroupVersionKind, f NewInteraceFunc) { + rm.gvkToNew[gvk] = f +} + +// NewResourceMapper returns a new map. +// This is required a we cannot push to uninitialized map. +func NewResourceMapper() *ResourceMapper { + m := map[schema.GroupVersionKind]NewInteraceFunc{} + return &ResourceMapper{ + l: &sync.Mutex{}, + gvkToNew: m, + } +} + type MetaV1ObjectModifierFunc func(metav1.Object) +// NewInteraceFunc returns an Interface. +// It requires rest Config that can be used to create a client +// and the Manifest. +type NewInteraceFunc func(rest *rest.Config, m lib.Manifest) Interface + type Interface interface { - Do(MetaV1ObjectModifierFunc) error + WithModifier(MetaV1ObjectModifierFunc) Interface + Do() error } -func New(rest *rest.Config, m lib.Manifest) (Interface, error) { - group, version := m.GVK.Group, m.GVK.Version - switch { - case group == batchv1.SchemeGroupVersion.Group && version == batchv1.SchemeGroupVersion.Version: - return newBatchBuilder(rest, m) - case group == corev1.SchemeGroupVersion.Group && version == corev1.SchemeGroupVersion.Version: - return newCoreBuilder(rest, m) - case group == appsv1.SchemeGroupVersion.Group && version == appsv1.SchemeGroupVersion.Version: - return newAppsBuilder(rest, m) - case group == rbacv1.SchemeGroupVersion.Group && version == rbacv1.SchemeGroupVersion.Version: - return newRbacBuilder(rest, m) - case group == apiextv1beta1.SchemeGroupVersion.Group && version == apiextv1beta1.SchemeGroupVersion.Version: - return newAPIExtBuilder(rest, m) - default: - return nil, fmt.Errorf("no builder found for %s,%s", group, version) +// New returns Interface using the mapping stored in mapper for m Manifest. +func New(mapper *ResourceMapper, rest *rest.Config, m lib.Manifest) (Interface, error) { + f, ok := mapper.gvkToNew[m.GVK] + if !ok { + return nil, fmt.Errorf("No mapping found for gvk: %v", m.GVK) } + return f(rest, m), nil } diff --git a/lib/resourcebuilder/rbac.go b/lib/resourcebuilder/rbac.go index 65c7e3f1a..9189b55b5 100644 --- a/lib/resourcebuilder/rbac.go +++ b/lib/resourcebuilder/rbac.go @@ -1,8 +1,6 @@ package resourcebuilder import ( - "fmt" - "github.com/openshift/cluster-version-operator/lib" "github.com/openshift/cluster-version-operator/lib/resourceapply" "github.com/openshift/cluster-version-operator/lib/resourceread" @@ -10,60 +8,110 @@ import ( "k8s.io/client-go/rest" ) -func newRbacBuilder(config *rest.Config, m lib.Manifest) (Interface, error) { - kind := m.GVK.Kind - switch kind { - case clusterRoleKind: - return newClusterRoleBuilder(config, m), nil - case clusterRoleBindingKind: - return newClusterRoleBindingBuilder(config, m), nil - default: - return nil, fmt.Errorf("no Rbac builder found for %s", kind) - } -} - -const ( - clusterRoleKind = "ClusterRole" -) - type clusterRoleBuilder struct { - client *rbacclientv1.RbacV1Client - raw []byte + client *rbacclientv1.RbacV1Client + raw []byte + modifier MetaV1ObjectModifierFunc } -func newClusterRoleBuilder(config *rest.Config, m lib.Manifest) *clusterRoleBuilder { +func newClusterRoleBuilder(config *rest.Config, m lib.Manifest) Interface { return &clusterRoleBuilder{ client: rbacclientv1.NewForConfigOrDie(config), raw: m.Raw, } } -func (b *clusterRoleBuilder) Do(modifier MetaV1ObjectModifierFunc) error { +func (b *clusterRoleBuilder) WithModifier(f MetaV1ObjectModifierFunc) Interface { + b.modifier = f + return b +} + +func (b *clusterRoleBuilder) Do() error { clusterRole := resourceread.ReadClusterRoleV1OrDie(b.raw) - modifier(clusterRole) + if b.modifier != nil { + b.modifier(clusterRole) + } _, _, err := resourceapply.ApplyClusterRole(b.client, clusterRole) return err } -const ( - clusterRoleBindingKind = "ClusterRoleBinding" -) - type clusterRoleBindingBuilder struct { - client *rbacclientv1.RbacV1Client - raw []byte + client *rbacclientv1.RbacV1Client + raw []byte + modifier MetaV1ObjectModifierFunc } -func newClusterRoleBindingBuilder(config *rest.Config, m lib.Manifest) *clusterRoleBindingBuilder { +func newClusterRoleBindingBuilder(config *rest.Config, m lib.Manifest) Interface { return &clusterRoleBindingBuilder{ client: rbacclientv1.NewForConfigOrDie(config), raw: m.Raw, } } -func (b *clusterRoleBindingBuilder) Do(modifier MetaV1ObjectModifierFunc) error { +func (b *clusterRoleBindingBuilder) WithModifier(f MetaV1ObjectModifierFunc) Interface { + b.modifier = f + return b +} + +func (b *clusterRoleBindingBuilder) Do() error { clusterRoleBinding := resourceread.ReadClusterRoleBindingV1OrDie(b.raw) - modifier(clusterRoleBinding) + if b.modifier != nil { + b.modifier(clusterRoleBinding) + } _, _, err := resourceapply.ApplyClusterRoleBinding(b.client, clusterRoleBinding) return err } + +type roleBuilder struct { + client *rbacclientv1.RbacV1Client + raw []byte + modifier MetaV1ObjectModifierFunc +} + +func newRoleBuilder(config *rest.Config, m lib.Manifest) Interface { + return &roleBuilder{ + client: rbacclientv1.NewForConfigOrDie(config), + raw: m.Raw, + } +} + +func (b *roleBuilder) WithModifier(f MetaV1ObjectModifierFunc) Interface { + b.modifier = f + return b +} + +func (b *roleBuilder) Do() error { + role := resourceread.ReadRoleV1OrDie(b.raw) + if b.modifier != nil { + b.modifier(role) + } + _, _, err := resourceapply.ApplyRole(b.client, role) + return err +} + +type roleBindingBuilder struct { + client *rbacclientv1.RbacV1Client + raw []byte + modifier MetaV1ObjectModifierFunc +} + +func newRoleBindingBuilder(config *rest.Config, m lib.Manifest) Interface { + return &roleBindingBuilder{ + client: rbacclientv1.NewForConfigOrDie(config), + raw: m.Raw, + } +} + +func (b *roleBindingBuilder) WithModifier(f MetaV1ObjectModifierFunc) Interface { + b.modifier = f + return b +} + +func (b *roleBindingBuilder) Do() error { + roleBinding := resourceread.ReadRoleBindingV1OrDie(b.raw) + if b.modifier != nil { + b.modifier(roleBinding) + } + _, _, err := resourceapply.ApplyRoleBinding(b.client, roleBinding) + return err +} diff --git a/lib/resourcebuilder/register.go b/lib/resourcebuilder/register.go new file mode 100644 index 000000000..35c315506 --- /dev/null +++ b/lib/resourcebuilder/register.go @@ -0,0 +1,30 @@ +package resourcebuilder + +import ( + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + rbacv1beta1 "k8s.io/api/rbac/v1beta1" + apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" +) + +func init() { + rm := NewResourceMapper() + rm.RegisterGVK(apiextv1beta1.SchemeGroupVersion.WithKind("CustomResourceDefinition"), newCRDBuilder) + rm.RegisterGVK(appsv1.SchemeGroupVersion.WithKind("Deployment"), newDeploymentBuilder) + rm.RegisterGVK(batchv1.SchemeGroupVersion.WithKind("Job"), newJobBuilder) + rm.RegisterGVK(corev1.SchemeGroupVersion.WithKind("ServiceAccount"), newServiceAccountBuilder) + rm.RegisterGVK(corev1.SchemeGroupVersion.WithKind("ConfigMap"), newConfigMapBuilder) + rm.RegisterGVK(corev1.SchemeGroupVersion.WithKind("Namespace"), newNamespaceBuilder) + rm.RegisterGVK(rbacv1.SchemeGroupVersion.WithKind("ClusterRole"), newClusterRoleBuilder) + rm.RegisterGVK(rbacv1.SchemeGroupVersion.WithKind("ClusterRoleBinding"), newClusterRoleBindingBuilder) + rm.RegisterGVK(rbacv1.SchemeGroupVersion.WithKind("Role"), newRoleBuilder) + rm.RegisterGVK(rbacv1.SchemeGroupVersion.WithKind("RoleBinding"), newRoleBindingBuilder) + rm.RegisterGVK(rbacv1beta1.SchemeGroupVersion.WithKind("ClusterRole"), newClusterRoleBuilder) + rm.RegisterGVK(rbacv1beta1.SchemeGroupVersion.WithKind("ClusterRoleBinding"), newClusterRoleBindingBuilder) + rm.RegisterGVK(rbacv1beta1.SchemeGroupVersion.WithKind("Role"), newRoleBuilder) + rm.RegisterGVK(rbacv1beta1.SchemeGroupVersion.WithKind("RoleBinding"), newRoleBindingBuilder) + + rm.AddToMap(Mapper) +} diff --git a/lib/resourcemerge/cv.go b/lib/resourcemerge/cv.go index eed24e073..e921a60da 100644 --- a/lib/resourcemerge/cv.go +++ b/lib/resourcemerge/cv.go @@ -2,34 +2,8 @@ package resourcemerge import ( cvv1 "github.com/openshift/cluster-version-operator/pkg/apis/clusterversion.openshift.io/v1" - osv1 "github.com/openshift/cluster-version-operator/pkg/apis/operatorstatus.openshift.io/v1" - "k8s.io/apimachinery/pkg/api/equality" ) -func EnsureOperatorStatus(modified *bool, existing *osv1.OperatorStatus, required osv1.OperatorStatus) { - EnsureObjectMeta(modified, &existing.ObjectMeta, required.ObjectMeta) - if !equality.Semantic.DeepEqual(existing.Condition, required.Condition) { - *modified = true - existing.Condition = required.Condition - } - if existing.Version != required.Version { - *modified = true - existing.Version = required.Version - } - if !existing.LastUpdate.Equal(&required.LastUpdate) { - *modified = true - existing.LastUpdate = required.LastUpdate - } - if !equality.Semantic.DeepEqual(existing.Extension.Raw, required.Extension.Raw) { - *modified = true - existing.Extension.Raw = required.Extension.Raw - } - if !equality.Semantic.DeepEqual(existing.Extension.Object, required.Extension.Object) { - *modified = true - existing.Extension.Object = required.Extension.Object - } -} - func EnsureCVOConfig(modified *bool, existing *cvv1.CVOConfig, required cvv1.CVOConfig) { EnsureObjectMeta(modified, &existing.ObjectMeta, required.ObjectMeta) if existing.Upstream != required.Upstream { diff --git a/lib/resourcemerge/os.go b/lib/resourcemerge/os.go new file mode 100644 index 000000000..893d78b57 --- /dev/null +++ b/lib/resourcemerge/os.go @@ -0,0 +1,30 @@ +package resourcemerge + +import ( + osv1 "github.com/openshift/cluster-version-operator/pkg/apis/operatorstatus.openshift.io/v1" + "k8s.io/apimachinery/pkg/api/equality" +) + +func EnsureOperatorStatus(modified *bool, existing *osv1.OperatorStatus, required osv1.OperatorStatus) { + EnsureObjectMeta(modified, &existing.ObjectMeta, required.ObjectMeta) + if !equality.Semantic.DeepEqual(existing.Condition, required.Condition) { + *modified = true + existing.Condition = required.Condition + } + if existing.Version != required.Version { + *modified = true + existing.Version = required.Version + } + if !existing.LastUpdate.Equal(&required.LastUpdate) { + *modified = true + existing.LastUpdate = required.LastUpdate + } + if !equality.Semantic.DeepEqual(existing.Extension.Raw, required.Extension.Raw) { + *modified = true + existing.Extension.Raw = required.Extension.Raw + } + if !equality.Semantic.DeepEqual(existing.Extension.Object, required.Extension.Object) { + *modified = true + existing.Extension.Object = required.Extension.Object + } +} diff --git a/lib/resourcemerge/rbac.go b/lib/resourcemerge/rbac.go index 07e2ae372..e39e843ad 100644 --- a/lib/resourcemerge/rbac.go +++ b/lib/resourcemerge/rbac.go @@ -28,3 +28,27 @@ func EnsureClusterRole(modified *bool, existing *rbacv1.ClusterRole, required rb existing.Rules = required.Rules } } + +// EnsureRoleBinding ensures that the existing matches the required. +// modified is set to true when existing had to be updated with required. +func EnsureRoleBinding(modified *bool, existing *rbacv1.RoleBinding, required rbacv1.RoleBinding) { + EnsureObjectMeta(modified, &existing.ObjectMeta, required.ObjectMeta) + if !equality.Semantic.DeepEqual(existing.Subjects, required.Subjects) { + *modified = true + existing.Subjects = required.Subjects + } + if !equality.Semantic.DeepEqual(existing.RoleRef, required.RoleRef) { + *modified = true + existing.RoleRef = required.RoleRef + } +} + +// EnsureRole ensures that the existing matches the required. +// modified is set to true when existing had to be updated with required. +func EnsureRole(modified *bool, existing *rbacv1.Role, required rbacv1.Role) { + EnsureObjectMeta(modified, &existing.ObjectMeta, required.ObjectMeta) + if !equality.Semantic.DeepEqual(existing.Rules, required.Rules) { + *modified = true + existing.Rules = required.Rules + } +} diff --git a/lib/resourceread/core.go b/lib/resourceread/core.go index 53bfd9693..393356aea 100644 --- a/lib/resourceread/core.go +++ b/lib/resourceread/core.go @@ -34,3 +34,11 @@ func ReadServiceAccountV1OrDie(objBytes []byte) *corev1.ServiceAccount { } return requiredObj.(*corev1.ServiceAccount) } + +func ReadNamespaceV1OrDie(objBytes []byte) *corev1.Namespace { + requiredObj, err := runtime.Decode(coreCodecs.UniversalDecoder(corev1.SchemeGroupVersion), objBytes) + if err != nil { + panic(err) + } + return requiredObj.(*corev1.Namespace) +} diff --git a/lib/resourceread/rbac.go b/lib/resourceread/rbac.go index 343161e59..6eb4e4a3a 100644 --- a/lib/resourceread/rbac.go +++ b/lib/resourceread/rbac.go @@ -2,6 +2,7 @@ package resourceread import ( rbacv1 "k8s.io/api/rbac/v1" + rbacv1beta1 "k8s.io/api/rbac/v1beta1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" ) @@ -15,6 +16,9 @@ func init() { if err := rbacv1.AddToScheme(rbacScheme); err != nil { panic(err) } + if err := rbacv1beta1.AddToScheme(rbacScheme); err != nil { + panic(err) + } } // ReadClusterRoleBindingV1OrDie reads clusterrolebinding object from bytes. Panics on error. @@ -34,3 +38,21 @@ func ReadClusterRoleV1OrDie(objBytes []byte) *rbacv1.ClusterRole { } return requiredObj.(*rbacv1.ClusterRole) } + +// ReadRoleBindingV1OrDie reads clusterrolebinding object from bytes. Panics on error. +func ReadRoleBindingV1OrDie(objBytes []byte) *rbacv1.RoleBinding { + requiredObj, err := runtime.Decode(rbacCodecs.UniversalDecoder(rbacv1.SchemeGroupVersion), objBytes) + if err != nil { + panic(err) + } + return requiredObj.(*rbacv1.RoleBinding) +} + +// ReadRoleV1OrDie reads clusterole object from bytes. Panics on error. +func ReadRoleV1OrDie(objBytes []byte) *rbacv1.Role { + requiredObj, err := runtime.Decode(rbacCodecs.UniversalDecoder(rbacv1.SchemeGroupVersion), objBytes) + if err != nil { + panic(err) + } + return requiredObj.(*rbacv1.Role) +} From 08c865bc655ec3f70038a1c9682d5cc1abfab400 Mon Sep 17 00:00:00 2001 From: Abhinav Dahiya Date: Tue, 18 Sep 2018 19:05:03 -0700 Subject: [PATCH 4/5] pkg/cvo: add OperatorStatus resource builder --- pkg/cvo/os.go | 93 +++++++++++++++++++++++++++++++++++++++++++++++ pkg/cvo/status.go | 18 ++++----- 2 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 pkg/cvo/os.go diff --git a/pkg/cvo/os.go b/pkg/cvo/os.go new file mode 100644 index 000000000..118453195 --- /dev/null +++ b/pkg/cvo/os.go @@ -0,0 +1,93 @@ +package cvo + +import ( + "time" + + "github.com/golang/glog" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/rest" + + "github.com/openshift/cluster-version-operator/lib" + "github.com/openshift/cluster-version-operator/lib/resourcebuilder" + osv1 "github.com/openshift/cluster-version-operator/pkg/apis/operatorstatus.openshift.io/v1" + osclientv1 "github.com/openshift/cluster-version-operator/pkg/generated/clientset/versioned/typed/operatorstatus.openshift.io/v1" +) + +var ( + osScheme = runtime.NewScheme() + osCodecs = serializer.NewCodecFactory(osScheme) + + osMapper = resourcebuilder.NewResourceMapper() +) + +func init() { + if err := osv1.AddToScheme(osScheme); err != nil { + panic(err) + } + + osMapper.RegisterGVK(osv1.SchemeGroupVersion.WithKind("OperatorStatus"), newOperatorStatusBuilder) + osMapper.AddToMap(resourcebuilder.Mapper) +} + +// readOperatorStatusV1OrDie reads operatorstatus object from bytes. Panics on error. +func readOperatorStatusV1OrDie(objBytes []byte) *osv1.OperatorStatus { + requiredObj, err := runtime.Decode(osCodecs.UniversalDecoder(osv1.SchemeGroupVersion), objBytes) + if err != nil { + panic(err) + } + return requiredObj.(*osv1.OperatorStatus) +} + +type operatorStatusBuilder struct { + client *osclientv1.OperatorstatusV1Client + raw []byte + modifier resourcebuilder.MetaV1ObjectModifierFunc +} + +func newOperatorStatusBuilder(config *rest.Config, m lib.Manifest) resourcebuilder.Interface { + return &operatorStatusBuilder{ + client: osclientv1.NewForConfigOrDie(config), + raw: m.Raw, + } +} + +func (b *operatorStatusBuilder) WithModifier(f resourcebuilder.MetaV1ObjectModifierFunc) resourcebuilder.Interface { + b.modifier = f + return b +} + +func (b *operatorStatusBuilder) Do() error { + os := readOperatorStatusV1OrDie(b.raw) + if b.modifier != nil { + b.modifier(os) + } + + return waitForOperatorStatusToBeDone(b.client, os) +} + +const ( + osPollInternal = 1 * time.Second + osPollTimeout = 1 * time.Minute +) + +func waitForOperatorStatusToBeDone(client osclientv1.OperatorStatusesGetter, os *osv1.OperatorStatus) error { + return wait.Poll(osPollInternal, osPollTimeout, func() (bool, error) { + eos, err := client.OperatorStatuses(os.Namespace).Get(os.Name, metav1.GetOptions{}) + if err != nil { + return false, err + } + + if eos.Version == os.Version && eos.Condition.Type == osv1.OperatorStatusConditionTypeDone { + return true, nil + } + glog.V(4).Infof("OperatorStatus %s/%s is not reporting %s for version %s; it is reporting %s for version %s", + eos.Namespace, eos.Name, + osv1.OperatorStatusConditionTypeDone, os.Version, + eos.Condition.Type, eos.Version, + ) + return false, nil + }) +} diff --git a/pkg/cvo/status.go b/pkg/cvo/status.go index 9d81552fe..0ce76e7d4 100644 --- a/pkg/cvo/status.go +++ b/pkg/cvo/status.go @@ -17,16 +17,14 @@ func (optr *Operator) syncStatus(config *cvv1.CVOConfig, cond osv1.OperatorStatu return fmt.Errorf("invalid cond %s", cond.Type) } - updates, err := checkForUpdate(*config) - if err != nil { - return err - } var cvoUpdates []cvv1.Update - for _, update := range updates { - cvoUpdates = append(cvoUpdates, cvv1.Update{ - Version: update.Version.String(), - Payload: update.Payload, - }) + if updates, err := checkForUpdate(*config); err == nil { + for _, update := range updates { + cvoUpdates = append(cvoUpdates, cvv1.Update{ + Version: update.Version.String(), + Payload: update.Payload, + }) + } } status := &osv1.OperatorStatus{ @@ -44,7 +42,7 @@ func (optr *Operator) syncStatus(config *cvv1.CVOConfig, cond osv1.OperatorStatu }, }, } - _, _, err = resourceapply.ApplyOperatorStatusFromCache(optr.operatorStatusLister, optr.client.OperatorstatusV1(), status) + _, _, err := resourceapply.ApplyOperatorStatusFromCache(optr.operatorStatusLister, optr.client.OperatorstatusV1(), status) return err } From 847f71bcef14c0567dae2c46bd374704c709e85c Mon Sep 17 00:00:00 2001 From: Abhinav Dahiya Date: Tue, 18 Sep 2018 19:05:33 -0700 Subject: [PATCH 5/5] pkg/cvo: update to new updatepayload structure --- pkg/cvo/cvo.go | 47 +++++--- pkg/cvo/sync.go | 235 ++++----------------------------------- pkg/cvo/updatepayload.go | 185 ++++++++++++++++++++++++++++++ 3 files changed, 236 insertions(+), 231 deletions(-) create mode 100644 pkg/cvo/updatepayload.go diff --git a/pkg/cvo/cvo.go b/pkg/cvo/cvo.go index 526678a29..7e6e985a2 100644 --- a/pkg/cvo/cvo.go +++ b/pkg/cvo/cvo.go @@ -6,18 +6,11 @@ import ( "github.com/golang/glog" "github.com/google/uuid" - "github.com/openshift/cluster-version-operator/lib/resourceapply" - cvv1 "github.com/openshift/cluster-version-operator/pkg/apis/clusterversion.openshift.io/v1" - osv1 "github.com/openshift/cluster-version-operator/pkg/apis/operatorstatus.openshift.io/v1" - clientset "github.com/openshift/cluster-version-operator/pkg/generated/clientset/versioned" - cvinformersv1 "github.com/openshift/cluster-version-operator/pkg/generated/informers/externalversions/clusterversion.openshift.io/v1" - osinformersv1 "github.com/openshift/cluster-version-operator/pkg/generated/informers/externalversions/operatorstatus.openshift.io/v1" - cvlistersv1 "github.com/openshift/cluster-version-operator/pkg/generated/listers/clusterversion.openshift.io/v1" - oslistersv1 "github.com/openshift/cluster-version-operator/pkg/generated/listers/operatorstatus.openshift.io/v1" corev1 "k8s.io/api/core/v1" apiextclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" apiextinformersv1beta1 "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions/v1beta1" apiextlistersv1beta1 "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1beta1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" @@ -30,6 +23,15 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/workqueue" + + "github.com/openshift/cluster-version-operator/lib/resourceapply" + cvv1 "github.com/openshift/cluster-version-operator/pkg/apis/clusterversion.openshift.io/v1" + osv1 "github.com/openshift/cluster-version-operator/pkg/apis/operatorstatus.openshift.io/v1" + clientset "github.com/openshift/cluster-version-operator/pkg/generated/clientset/versioned" + cvinformersv1 "github.com/openshift/cluster-version-operator/pkg/generated/informers/externalversions/clusterversion.openshift.io/v1" + osinformersv1 "github.com/openshift/cluster-version-operator/pkg/generated/informers/externalversions/operatorstatus.openshift.io/v1" + cvlistersv1 "github.com/openshift/cluster-version-operator/pkg/generated/listers/clusterversion.openshift.io/v1" + oslistersv1 "github.com/openshift/cluster-version-operator/pkg/generated/listers/operatorstatus.openshift.io/v1" ) const ( @@ -39,11 +41,6 @@ const ( // // 5ms, 10ms, 20ms, 40ms, 80ms, 160ms, 320ms, 640ms, 1.3s, 2.6s, 5.1s, 10.2s, 20.4s, 41s, 82s maxRetries = 15 - - // installconfigKey is the key in ConfigMap that stores the InstallConfig. - installconfigKey = "installconfig" - - workQueueKey = "kube-system/installconfig" ) // ownerKind contains the schema.GroupVersionKind for type that owns objects managed by CVO. @@ -146,6 +143,7 @@ func (optr *Operator) Run(workers int, stopCh <-chan struct{}) { } func (optr *Operator) eventHandler() cache.ResourceEventHandler { + workQueueKey := fmt.Sprintf("%s/%s", optr.namespace, optr.name) return cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { optr.queue.Add(workQueueKey) }, UpdateFunc: func(old, new interface{}) { optr.queue.Add(workQueueKey) }, @@ -197,20 +195,36 @@ func (optr *Operator) sync(key string) error { }() // We always run this to make sure CVOConfig can be synced. - if err := optr.syncCVOCRDs(); err != nil { + if err := optr.syncCustomResourceDefinitions(); err != nil { + return err + } + + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { return err } - config, err := optr.getConfig() + var obj *cvv1.CVOConfig + obj, err = optr.cvoConfigLister.CVOConfigs(namespace).Get(name) + if apierrors.IsNotFound(err) { + obj, err = optr.getConfig() + } if err != nil { return err } + config := &cvv1.CVOConfig{} + obj.DeepCopyInto(config) + if err := optr.syncStatus(config, osv1.OperatorStatusCondition{Type: osv1.OperatorStatusConditionTypeWorking, Message: fmt.Sprintf("Working towards %s", config)}); err != nil { return err } - payload, err := optr.syncUpdatePayloadContents(updatePayloadsPathPrefix, config) + payloadDir, err := optr.updatePayloadDir(config) + if err != nil { + return err + } + payload, err := optr.loadUpdatePayload(payloadDir) if err != nil { return err } @@ -223,7 +237,6 @@ func (optr *Operator) sync(key string) error { } func (optr *Operator) getConfig() (*cvv1.CVOConfig, error) { - // XXX: fetch upstream, channel, cluster ID from InstallConfig upstream := cvv1.URL("http://localhost:8080/graph") channel := "fast" id, _ := uuid.NewRandom() diff --git a/pkg/cvo/sync.go b/pkg/cvo/sync.go index e482cc28d..0489b69da 100644 --- a/pkg/cvo/sync.go +++ b/pkg/cvo/sync.go @@ -2,190 +2,45 @@ package cvo import ( "fmt" - "os" - "path/filepath" - "sort" "time" "github.com/golang/glog" - "github.com/openshift/cluster-version-operator/lib" - "github.com/openshift/cluster-version-operator/lib/resourceapply" - "github.com/openshift/cluster-version-operator/lib/resourcebuilder" - "github.com/openshift/cluster-version-operator/pkg/apis" - "github.com/openshift/cluster-version-operator/pkg/apis/clusterversion.openshift.io/v1" - appsv1 "k8s.io/api/apps/v1" - batchv1 "k8s.io/api/batch/v1" - corev1 "k8s.io/api/core/v1" apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" - randutil "k8s.io/apimachinery/pkg/util/rand" "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/utils/pointer" + + "github.com/openshift/cluster-version-operator/lib/resourceapply" + "github.com/openshift/cluster-version-operator/lib/resourcebuilder" + "github.com/openshift/cluster-version-operator/pkg/apis" + cvv1 "github.com/openshift/cluster-version-operator/pkg/apis/clusterversion.openshift.io/v1" ) -func (optr *Operator) syncUpdatePayload(config *v1.CVOConfig, payload [][]lib.Manifest) error { - for _, manifests := range payload { - for _, manifest := range manifests { - glog.V(4).Infof("Running sync for (%s) %s/%s", manifest.GVK.String(), manifest.Object().GetNamespace(), manifest.Object().GetName()) - b, err := resourcebuilder.New(optr.restConfig, manifest) - if err != nil { - return err - } - if err := b.Do(ownerRefModifier(config)); err != nil { - return err - } - glog.V(4).Infof("Done syncing for (%s) %s/%s", manifest.GVK.String(), manifest.Object().GetNamespace(), manifest.Object().GetName()) +func (optr *Operator) syncUpdatePayload(config *cvv1.CVOConfig, payload *updatePayload) error { + for _, manifest := range payload.manifests { + taskName := fmt.Sprintf("(%s) %s/%s", manifest.GVK.String(), manifest.Object().GetNamespace(), manifest.Object().GetName()) + glog.V(4).Infof("Running sync for %s", taskName) + glog.V(4).Infof("Manifest: %s", string(manifest.Raw)) + b, err := resourcebuilder.New(resourcebuilder.Mapper, optr.restConfig, manifest) + if err != nil { + return fmt.Errorf("error creating New resourcebuilder for %s: %v", taskName, err) + } + if err := b.WithModifier(ownerRefModifier(config)).Do(); err != nil { + return fmt.Errorf("error running apply for %s: %v", taskName, err) } + glog.V(4).Infof("Done syncing for %s", taskName) } return nil } -func ownerRefModifier(config *v1.CVOConfig) resourcebuilder.MetaV1ObjectModifierFunc { +func ownerRefModifier(config *cvv1.CVOConfig) resourcebuilder.MetaV1ObjectModifierFunc { oref := metav1.NewControllerRef(config, ownerKind) return func(obj metav1.Object) { obj.SetOwnerReferences([]metav1.OwnerReference{*oref}) } } -const ( - updatePayloadsPathPrefix = "/etc/cvo/update-payloads" -) - -func (optr *Operator) syncUpdatePayloadContents(pathprefix string, config *v1.CVOConfig) ([][]lib.Manifest, error) { - if !updatedesired(config.DesiredUpdate) { - return nil, nil - } - dv := config.DesiredUpdate.Version - dp := config.DesiredUpdate.Payload - - dirpath := filepath.Join(pathprefix, dv) - _, err := os.Stat(dirpath) - if err != nil && !os.IsNotExist(err) { - return nil, err - } - if os.IsNotExist(err) { - if err := optr.fetchUpdatePayloadToPath(pathprefix, config); err != nil { - return nil, err - } - } - - // read dirpath to manifests: - // For each operator in payload the manifests should be ordered as: - // 1. CRDs - // 2. others - mmap, err := lib.LoadManifests(dirpath) - if err != nil { - return nil, err - } - - if len(mmap) == 0 { - return nil, fmt.Errorf("empty update payload %s", dp) - } - - sortedkeys := []string{} - for k := range mmap { - sortedkeys = append(sortedkeys, k) - } - sort.Strings(sortedkeys) - - payload := [][]lib.Manifest{} - for _, k := range sortedkeys { - ordered := orderManifests(mmap[k]) - payload = append(payload, ordered) - } - return payload, nil -} - -func (optr *Operator) fetchUpdatePayloadToPath(pathprefix string, config *v1.CVOConfig) error { - if !updatedesired(config.DesiredUpdate) { - return fmt.Errorf("error DesiredUpdate is empty") - } - var ( - version = config.DesiredUpdate.Version - payload = config.DesiredUpdate.Payload - name = fmt.Sprintf("%s-%s-%s", optr.name, version, randutil.String(5)) - namespace = optr.namespace - deadline = pointer.Int64Ptr(2 * 60) - nodeSelectorKey = "node-role.kubernetes.io/master" - nodename = optr.nodename - vpath = filepath.Join(pathprefix, version) - cmd = []string{"/bin/sh"} - args = []string{"-c", fmt.Sprintf("mv /manifests %s", vpath)} - ) - - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Spec: batchv1.JobSpec{ - ActiveDeadlineSeconds: deadline, - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{{ - Name: "payload", - Image: payload, - Command: cmd, - Args: args, - VolumeMounts: []corev1.VolumeMount{{ - MountPath: pathprefix, - Name: "payload", - }}, - }}, - Volumes: []corev1.Volume{{ - Name: "payload", - VolumeSource: corev1.VolumeSource{ - HostPath: &corev1.HostPathVolumeSource{ - Path: pathprefix, - }, - }, - }}, - NodeName: nodename, - NodeSelector: map[string]string{ - nodeSelectorKey: "", - }, - Tolerations: []corev1.Toleration{{ - Key: nodeSelectorKey, - }}, - RestartPolicy: corev1.RestartPolicyOnFailure, - }, - }, - }, - } - - _, err := optr.kubeClient.BatchV1().Jobs(job.Namespace).Create(job) - if err != nil { - return err - } - return resourcebuilder.WaitForJobCompletion(optr.kubeClient.BatchV1(), job) -} - -func orderManifests(manifests []lib.Manifest) []lib.Manifest { - var crds, others []lib.Manifest - for _, m := range manifests { - group, version, kind := m.GVK.Group, m.GVK.Version, m.GVK.Kind - switch { - case group == apiextv1beta1.SchemeGroupVersion.Group && version == apiextv1beta1.SchemeGroupVersion.Version && kind == resourcebuilder.CRDKind: - crds = append(crds, m) - default: - others = append(others, m) - } - } - - out := []lib.Manifest{} - out = append(out, crds...) - out = append(out, others...) - return out -} - -func updatedesired(desired v1.Update) bool { - return desired.Payload != "" && - desired.Version != "" -} - -func (optr *Operator) syncCVOCRDs() error { +func (optr *Operator) syncCustomResourceDefinitions() error { crds := []*apiextv1beta1.CustomResourceDefinition{{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("operatorstatuses.%s", apis.OperatorStatusGroupName), @@ -202,22 +57,6 @@ func (optr *Operator) syncCVOCRDs() error { ListKind: "OperatorStatusList", }, }, - }, { - ObjectMeta: metav1.ObjectMeta{ - Name: fmt.Sprintf("cvoconfigs.%s", apis.ClusterVersionGroupName), - Namespace: metav1.NamespaceDefault, - }, - Spec: apiextv1beta1.CustomResourceDefinitionSpec{ - Group: apis.ClusterVersionGroupName, - Version: "v1", - Scope: "Namespaced", - Names: apiextv1beta1.CustomResourceDefinitionNames{ - Plural: "cvoconfigs", - Singular: "cvoconfig", - Kind: "CVOConfig", - ListKind: "CVOConfigList", - }, - }, }} for _, crd := range crds { @@ -235,10 +74,8 @@ func (optr *Operator) syncCVOCRDs() error { } const ( - deploymentRolloutPollInterval = time.Second - deploymentRolloutTimeout = 5 * time.Minute - customResourceReadyInterval = time.Second - customResourceReadyTimeout = 1 * time.Minute + customResourceReadyInterval = time.Second + customResourceReadyTimeout = 1 * time.Minute ) func (optr *Operator) waitForCustomResourceDefinition(resource *apiextv1beta1.CustomResourceDefinition) error { @@ -262,33 +99,3 @@ func (optr *Operator) waitForCustomResourceDefinition(resource *apiextv1beta1.Cu return false, nil }) } - -func (optr *Operator) waitForDeploymentRollout(resource *appsv1.Deployment) error { - return wait.Poll(deploymentRolloutPollInterval, deploymentRolloutTimeout, func() (bool, error) { - d, err := optr.deployLister.Deployments(resource.Namespace).Get(resource.Name) - if errors.IsNotFound(err) { - // exit early to recreate the deployment. - return false, err - } - if err != nil { - // Do not return error here, as we could be updating the API Server itself, in which case we - // want to continue waiting. - glog.Errorf("error getting Deployment %s during rollout: %v", resource.Name, err) - return false, nil - } - - if d.DeletionTimestamp != nil { - return false, fmt.Errorf("Deployment %s is being deleted", resource.Name) - } - - if d.Generation <= d.Status.ObservedGeneration && d.Status.UpdatedReplicas == d.Status.Replicas && d.Status.UnavailableReplicas == 0 { - return true, nil - } - glog.V(4).Infof("Deployment %s is not ready. status: (replicas: %d, updated: %d, ready: %d, unavailable: %d)", d.Name, d.Status.Replicas, d.Status.UpdatedReplicas, d.Status.ReadyReplicas, d.Status.UnavailableReplicas) - return false, nil - }) -} - -func intOrStringPtr(v intstr.IntOrString) *intstr.IntOrString { - return &v -} diff --git a/pkg/cvo/updatepayload.go b/pkg/cvo/updatepayload.go new file mode 100644 index 000000000..00c10478b --- /dev/null +++ b/pkg/cvo/updatepayload.go @@ -0,0 +1,185 @@ +package cvo + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + randutil "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/utils/pointer" + + "github.com/golang/glog" + "github.com/openshift/cluster-version-operator/lib" + "github.com/openshift/cluster-version-operator/lib/resourcebuilder" + cvv1 "github.com/openshift/cluster-version-operator/pkg/apis/clusterversion.openshift.io/v1" +) + +type updatePayload struct { + // XXX: cincinatti.json struct + + // XXX: image-references + + manifests []lib.Manifest +} + +const ( + defaultUpdatePayloadDir = "/etc/cvo/updatepayload" + targetUpdatePayloadsDir = "/etc/cvo/updatepayloads" + + cincinnatiJSONFile = "cincinnati.json" + imageReferencesFile = "image-references" +) + +func (optr *Operator) loadUpdatePayload(dir string) (*updatePayload, error) { + glog.V(4).Info("Loading updatepayload from %q", dir) + if err := validateUpdatePayload(dir); err != nil { + return nil, err + } + + // XXX: load cincinnatiJSONFile + cjf := filepath.Join(dir, cincinnatiJSONFile) + // XXX: load imageReferencesFile + irf := filepath.Join(dir, imageReferencesFile) + + var mfs []string + skipFiles := sets.NewString(cjf, irf) + files, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + for _, file := range files { + if file.IsDir() { + continue + } + + p := filepath.Join(dir, file.Name()) + if skipFiles.Has(p) { + continue + } + mfs = append(mfs, p) + } + manifests, err := lib.ManifestsFromFiles(mfs) + if err != nil { + return nil, err + } + + return &updatePayload{ + manifests: manifests, + }, nil +} + +func (optr *Operator) updatePayloadDir(config *cvv1.CVOConfig) (string, error) { + ret := defaultUpdatePayloadDir + tdir, err := optr.targetUpdatePayloadDir(config) + if err != nil { + return "", fmt.Errorf("error fetching targetUpdatePayloadDir: %v", err) + } + if len(tdir) > 0 { + ret = tdir + } + return ret, nil +} + +func (optr *Operator) targetUpdatePayloadDir(config *cvv1.CVOConfig) (string, error) { + if !isTargetSet(config.DesiredUpdate) { + return "", nil + } + + // XXX: check if target and default versions are equal + // requires cincinati.json + + tdir := filepath.Join(targetUpdatePayloadsDir, config.DesiredUpdate.Version) + _, err := os.Stat(tdir) + if err != nil && !os.IsNotExist(err) { + return "", err + } + + if os.IsNotExist(err) { + if err := optr.fetchUpdatePayloadToDir(tdir, config); err != nil { + return "", err + } + } + if err := validateUpdatePayload(tdir); err != nil { + return "", err + } + return tdir, nil +} + +func validateUpdatePayload(dir string) error { + // XXX: validate that cincinnati.json is correct + // validate image-references files is correct. + return nil +} + +func (optr *Operator) fetchUpdatePayloadToDir(dir string, config *cvv1.CVOConfig) error { + var ( + version = config.DesiredUpdate.Version + payload = config.DesiredUpdate.Payload + name = fmt.Sprintf("%s-%s-%s", optr.name, version, randutil.String(5)) + namespace = optr.namespace + deadline = pointer.Int64Ptr(2 * 60) + nodeSelectorKey = "node-role.kubernetes.io/master" + nodename = optr.nodename + cmd = []string{"/bin/sh"} + args = []string{"-c", fmt.Sprintf("cp -r %s %s", defaultUpdatePayloadDir, dir)} + ) + + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: batchv1.JobSpec{ + ActiveDeadlineSeconds: deadline, + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "payload", + Image: payload, + Command: cmd, + Args: args, + VolumeMounts: []corev1.VolumeMount{{ + MountPath: targetUpdatePayloadsDir, + Name: "payloads", + }}, + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.BoolPtr(true), + }, + }}, + Volumes: []corev1.Volume{{ + Name: "payloads", + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: targetUpdatePayloadsDir, + }, + }, + }}, + NodeName: nodename, + NodeSelector: map[string]string{ + nodeSelectorKey: "", + }, + Tolerations: []corev1.Toleration{{ + Key: nodeSelectorKey, + }}, + RestartPolicy: corev1.RestartPolicyOnFailure, + }, + }, + }, + } + + _, err := optr.kubeClient.BatchV1().Jobs(job.Namespace).Create(job) + if err != nil { + return err + } + return resourcebuilder.WaitForJobCompletion(optr.kubeClient.BatchV1(), job) +} + +func isTargetSet(desired cvv1.Update) bool { + return desired.Payload != "" && + desired.Version != "" +}