diff --git a/.gitignore b/.gitignore index af02ef71f..991fe754f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,6 @@ _testmain.go *.test *.prof /vendor/ -/.glide/ /build/ /docker/build/ .idea @@ -87,6 +86,9 @@ coverage.xml .hypothesis/ .pytest_cache/ +# e2e tests +e2e/manifests + # Translations *.mo *.pot diff --git a/.travis.yml b/.travis.yml index 9740b43c8..589eb03a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ go: - "1.12.x" before_install: - - go get github.com/Masterminds/glide - go get github.com/mattn/goveralls install: diff --git a/Makefile b/Makefile index a78ec7224..dc1c790fe 100644 --- a/Makefile +++ b/Makefile @@ -78,8 +78,9 @@ scm-source.json: .git echo '{\n "url": "git:$(GITURL)",\n "revision": "$(GITHEAD)",\n "author": "$(USER)",\n "status": "$(GITSTATUS)"\n}' > scm-source.json tools: - @go get -u honnef.co/go/tools/cmd/staticcheck - @go get -u github.com/Masterminds/glide + GO111MODULE=on go get -u honnef.co/go/tools/cmd/staticcheck + GO111MODULE=on go get k8s.io/client-go@kubernetes-1.16.3 + GO111MODULE=on go mod tidy fmt: @gofmt -l -w -s $(DIRS) @@ -88,12 +89,12 @@ vet: @go vet $(PKG) @staticcheck $(PKG) -deps: - @glide install --strip-vendor +deps: tools + GO111MODULE=on go mod vendor test: hack/verify-codegen.sh - @go test ./... + GO111MODULE=on go test ./... -e2e: - cd e2e; make tools test +e2e: docker # build operator image to be tested + cd e2e; make tools test clean diff --git a/charts/postgres-operator/Chart.yaml b/charts/postgres-operator/Chart.yaml index ae9bb855e..08e242a53 100644 --- a/charts/postgres-operator/Chart.yaml +++ b/charts/postgres-operator/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v1 name: postgres-operator -version: 1.2.0 -appVersion: 1.2.0 +version: 1.3.0 +appVersion: 1.3.0 home: https://github.com/zalando/postgres-operator description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes keywords: @@ -13,8 +13,6 @@ keywords: maintainers: - name: Zalando email: opensource@zalando.de -- name: kimxogus - email: kgyoo8232@gmail.com sources: - https://github.com/zalando/postgres-operator engine: gotpl diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml new file mode 100644 index 000000000..c97e246ab --- /dev/null +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -0,0 +1,316 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: operatorconfigurations.acid.zalan.do + labels: + app.kubernetes.io/name: postgres-operator + annotations: + "helm.sh/hook": crd-install +spec: + group: acid.zalan.do + names: + kind: OperatorConfiguration + listKind: OperatorConfigurationList + plural: operatorconfigurations + singular: operatorconfiguration + shortNames: + - opconfig + additionalPrinterColumns: + - name: Image + type: string + description: Spilo image to be used for Pods + JSONPath: .configuration.docker_image + - name: Cluster-Label + type: string + description: Label for K8s resources created by operator + JSONPath: .configuration.kubernetes.cluster_name_label + - name: Service-Account + type: string + description: Name of service account to be used + JSONPath: .configuration.kubernetes.pod_service_account_name + - name: Min-Instances + type: integer + description: Minimum number of instances per Postgres cluster + JSONPath: .configuration.min_instances + - name: Age + type: date + JSONPath: .metadata.creationTimestamp + scope: Namespaced + subresources: + status: {} + version: v1 + validation: + openAPIV3Schema: + type: object + required: + - kind + - apiVersion + - configuration + properties: + kind: + type: string + enum: + - OperatorConfiguration + apiVersion: + type: string + enum: + - acid.zalan.do/v1 + configuration: + type: object + properties: + docker_image: + type: string + enable_crd_validation: + type: boolean + enable_shm_volume: + type: boolean + etcd_host: + type: string + max_instances: + type: integer + minimum: -1 # -1 = disabled + min_instances: + type: integer + minimum: -1 # -1 = disabled + resync_period: + type: string + repair_period: + type: string + set_memory_request_to_limit: + type: boolean + sidecar_docker_images: + type: object + additionalProperties: + type: string + workers: + type: integer + minimum: 1 + users: + type: object + properties: + replication_username: + type: string + super_username: + type: string + kubernetes: + type: object + properties: + cluster_domain: + type: string + cluster_labels: + type: object + additionalProperties: + type: string + cluster_name_label: + type: string + custom_pod_annotations: + type: object + additionalProperties: + type: string + enable_init_containers: + type: boolean + enable_pod_antiaffinity: + type: boolean + enable_pod_disruption_budget: + type: boolean + enable_sidecars: + type: boolean + infrastructure_roles_secret_name: + type: string + inherited_labels: + type: array + items: + type: string + master_pod_move_timeout: + type: string + node_readiness_label: + type: object + additionalProperties: + type: string + oauth_token_secret_name: + type: string + pdb_name_format: + type: string + pod_antiaffinity_topology_key: + type: string + pod_environment_configmap: + type: string + pod_management_policy: + type: string + enum: + - "ordered_ready" + - "parallel" + pod_priority_class_name: + type: string + pod_role_label: + type: string + pod_service_account_definition: + type: string + pod_service_account_name: + type: string + pod_service_account_role_binding_definition: + type: string + pod_terminate_grace_period: + type: string + secret_name_template: + type: string + spilo_fsgroup: + type: integer + spilo_privileged: + type: boolean + toleration: + type: object + additionalProperties: + type: string + watched_namespace: + type: string + postgres_pod_resources: + type: object + properties: + default_cpu_limit: + type: string + pattern: '^(\d+m|\d+(\.\d{1,3})?)$' + default_cpu_request: + type: string + pattern: '^(\d+m|\d+(\.\d{1,3})?)$' + default_memory_limit: + type: string + pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' + default_memory_request: + type: string + pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' + timeouts: + type: object + properties: + pod_label_wait_timeout: + type: string + pod_deletion_wait_timeout: + type: string + ready_wait_interval: + type: string + ready_wait_timeout: + type: string + resource_check_interval: + type: string + resource_check_timeout: + type: string + load_balancer: + type: object + properties: + custom_service_annotations: + type: object + additionalProperties: + type: string + db_hosted_zone: + type: string + enable_master_load_balancer: + type: boolean + enable_replica_load_balancer: + type: boolean + master_dns_name_format: + type: string + replica_dns_name_format: + type: string + aws_or_gcp: + type: object + properties: + additional_secret_mount: + type: string + additional_secret_mount_path: + type: string + aws_region: + type: string + kube_iam_role: + type: string + log_s3_bucket: + type: string + wal_s3_bucket: + type: string + logical_backup: + type: object + properties: + logical_backup_docker_image: + type: string + logical_backup_s3_access_key_id: + type: string + logical_backup_s3_bucket: + type: string + logical_backup_s3_endpoint: + type: string + logical_backup_s3_secret_access_key: + type: string + logical_backup_s3_sse: + type: string + logical_backup_schedule: + type: string + pattern: '^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$' + debug: + type: object + properties: + debug_logging: + type: boolean + enable_database_access: + type: boolean + teams_api: + type: object + properties: + enable_admin_role_for_users: + type: boolean + enable_team_superuser: + type: boolean + enable_teams_api: + type: boolean + pam_configuration: + type: string + pam_role_name: + type: string + postgres_superuser_teams: + type: array + items: + type: string + protected_role_names: + type: array + items: + type: string + team_admin_role: + type: string + team_api_role_configuration: + type: object + additionalProperties: + type: string + teams_api_url: + type: string + logging_rest_api: + type: object + properties: + api_port: + type: integer + cluster_history_entries: + type: integer + ring_log_lines: + type: integer + scalyr: + type: object + properties: + scalyr_api_key: + type: string + scalyr_cpu_limit: + type: string + pattern: '^(\d+m|\d+(\.\d{1,3})?)$' + scalyr_cpu_request: + type: string + pattern: '^(\d+m|\d+(\.\d{1,3})?)$' + scalyr_image: + type: string + scalyr_memory_limit: + type: string + pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' + scalyr_memory_request: + type: string + pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' + scalyr_server_url: + type: string + status: + type: object + additionalProperties: + type: string diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml new file mode 100644 index 000000000..198afe119 --- /dev/null +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -0,0 +1,363 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: postgresqls.acid.zalan.do + labels: + app.kubernetes.io/name: postgres-operator + annotations: + "helm.sh/hook": crd-install +spec: + group: acid.zalan.do + names: + kind: postgresql + listKind: postgresqlList + plural: postgresqls + singular: postgresql + shortNames: + - pg + additionalPrinterColumns: + - name: Team + type: string + description: Team responsible for Postgres CLuster + JSONPath: .spec.teamId + - name: Version + type: string + description: PostgreSQL version + JSONPath: .spec.postgresql.version + - name: Pods + type: integer + description: Number of Pods per Postgres cluster + JSONPath: .spec.numberOfInstances + - name: Volume + type: string + description: Size of the bound volume + JSONPath: .spec.volume.size + - name: CPU-Request + type: string + description: Requested CPU for Postgres containers + JSONPath: .spec.resources.requests.cpu + - name: Memory-Request + type: string + description: Requested memory for Postgres containers + JSONPath: .spec.resources.requests.memory + - name: Age + type: date + JSONPath: .metadata.creationTimestamp + - name: Status + type: string + description: Current sync status of postgresql resource + JSONPath: .status.PostgresClusterStatus + scope: Namespaced + subresources: + status: {} + version: v1 + validation: + openAPIV3Schema: + type: object + required: + - kind + - apiVersion + - spec + properties: + kind: + type: string + enum: + - postgresql + apiVersion: + type: string + enum: + - acid.zalan.do/v1 + spec: + type: object + required: + - numberOfInstances + - teamId + - postgresql + properties: + allowedSourceRanges: + type: array + nullable: true + items: + type: string + pattern: '^(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])$' + clone: + type: object + required: + - cluster + properties: + cluster: + type: string + s3_endpoint: + type: string + s3_access_key_id: + type: string + s3_secret_access_key: + type: string + s3_force_path_style: + type: string + s3_wal_path: + type: string + timestamp: + type: string + pattern: '^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([+-]([01][0-9]|2[0-3]):[0-5][0-9]))$' + # The regexp matches the date-time format (RFC 3339 Section 5.6) that specifies a timezone as an offset relative to UTC + # Example: 1996-12-19T16:39:57-08:00 + # Note: this field requires a timezone + uid: + format: uuid + type: string + databases: + type: object + additionalProperties: + type: string + # Note: usernames specified here as database owners must be declared in the users key of the spec key. + dockerImage: + type: string + enableLogicalBackup: + type: boolean + enableMasterLoadBalancer: + type: boolean + enableReplicaLoadBalancer: + type: boolean + enableShmVolume: + type: boolean + init_containers: # deprecated + type: array + nullable: true + items: + type: object + additionalProperties: true + initContainers: + type: array + nullable: true + items: + type: object + additionalProperties: true + logicalBackupSchedule: + type: string + pattern: '^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$' + maintenanceWindows: + type: array + items: + type: string + pattern: '^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ *$' + numberOfInstances: + type: integer + minimum: 0 + patroni: + type: object + properties: + initdb: + type: object + additionalProperties: + type: string + pg_hba: + type: array + items: + type: string + slots: + type: object + additionalProperties: + type: object + additionalProperties: + type: string + ttl: + type: integer + loop_wait: + type: integer + retry_timeout: + type: integer + maximum_lag_on_failover: + type: integer + podAnnotations: + type: object + additionalProperties: + type: string + pod_priority_class_name: # deprecated + type: string + podPriorityClassName: + type: string + postgresql: + type: object + required: + - version + properties: + version: + type: string + enum: + - "9.3" + - "9.4" + - "9.5" + - "9.6" + - "10" + - "11" + - "12" + parameters: + type: object + additionalProperties: + type: string + replicaLoadBalancer: # deprecated + type: boolean + resources: + type: object + required: + - requests + - limits + properties: + limits: + type: object + required: + - cpu + - memory + properties: + cpu: + type: string + # Decimal natural followed by m, or decimal natural followed by + # dot followed by up to three decimal digits. + # + # This is because the Kubernetes CPU resource has millis as the + # maximum precision. The actual values are checked in code + # because the regular expression would be huge and horrible and + # not very helpful in validation error messages; this one checks + # only the format of the given number. + # + # https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu + pattern: '^(\d+m|\d+(\.\d{1,3})?)$' + # Note: the value specified here must not be zero or be lower + # than the corresponding request. + memory: + type: string + # You can express memory as a plain integer or as a fixed-point + # integer using one of these suffixes: E, P, T, G, M, k. You can + # also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki + # + # https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory + pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' + # Note: the value specified here must not be zero or be lower + # than the corresponding request. + requests: + type: object + required: + - cpu + - memory + properties: + cpu: + type: string + # Decimal natural followed by m, or decimal natural followed by + # dot followed by up to three decimal digits. + # + # This is because the Kubernetes CPU resource has millis as the + # maximum precision. The actual values are checked in code + # because the regular expression would be huge and horrible and + # not very helpful in validation error messages; this one checks + # only the format of the given number. + # + # https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu + pattern: '^(\d+m|\d+(\.\d{1,3})?)$' + # Note: the value specified here must not be zero or be higher + # than the corresponding limit. + memory: + type: string + # You can express memory as a plain integer or as a fixed-point + # integer using one of these suffixes: E, P, T, G, M, k. You can + # also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki + # + # https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory + pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' + # Note: the value specified here must not be zero or be higher + # than the corresponding limit. + sidecars: + type: array + nullable: true + items: + type: object + additionalProperties: true + spiloFSGroup: + type: integer + standby: + type: object + required: + - s3_wal_path + properties: + s3_wal_path: + type: string + teamId: + type: string + tolerations: + type: array + items: + type: object + required: + - key + - operator + - effect + properties: + key: + type: string + operator: + type: string + enum: + - Equal + - Exists + value: + type: string + effect: + type: string + enum: + - NoExecute + - NoSchedule + - PreferNoSchedule + tolerationSeconds: + type: integer + useLoadBalancer: # deprecated + type: boolean + users: + type: object + additionalProperties: + type: array + nullable: true + description: "Role flags specified here must not contradict each other" + items: + type: string + enum: + - bypassrls + - BYPASSRLS + - nobypassrls + - NOBYPASSRLS + - createdb + - CREATEDB + - nocreatedb + - NOCREATEDB + - createrole + - CREATEROLE + - nocreaterole + - NOCREATEROLE + - inherit + - INHERIT + - noinherit + - NOINHERIT + - login + - LOGIN + - nologin + - NOLOGIN + - replication + - REPLICATION + - noreplication + - NOREPLICATION + - superuser + - SUPERUSER + - nosuperuser + - NOSUPERUSER + volume: + type: object + required: + - size + properties: + size: + type: string + pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' + # Note: the value specified here must not be zero. + storageClass: + type: string + subPath: + type: string diff --git a/charts/postgres-operator/index.yaml b/charts/postgres-operator/index.yaml index b549f1220..84502f6a6 100644 --- a/charts/postgres-operator/index.yaml +++ b/charts/postgres-operator/index.yaml @@ -1,13 +1,34 @@ apiVersion: v1 entries: postgres-operator: + - apiVersion: v1 + appVersion: 1.3.0 + created: "2019-12-17T12:58:49.477140129+01:00" + description: Postgres Operator creates and manages PostgreSQL clusters running + in Kubernetes + digest: 7e788fd37daec76a01f6d6f9fe5be5b54f5035e4eba0041e80a760d656537325 + home: https://github.com/zalando/postgres-operator + keywords: + - postgres + - operator + - cloud-native + - patroni + - spilo + maintainers: + - email: opensource@zalando.de + name: Zalando + name: postgres-operator + sources: + - https://github.com/zalando/postgres-operator + urls: + - postgres-operator-1.3.0.tgz + version: 1.3.0 - apiVersion: v1 appVersion: 1.2.0 - created: "2019-08-13T17:33:32.735021423+02:00" + created: "2019-12-17T12:58:49.475844233+01:00" description: Postgres Operator creates and manages PostgreSQL clusters running in Kubernetes digest: d10710c7cf19f4e266e7704f5d1e98dcfc61bee3919522326c35c22ca7d2f2bf - engine: gotpl home: https://github.com/zalando/postgres-operator keywords: - postgres @@ -26,4 +47,4 @@ entries: urls: - postgres-operator-1.2.0.tgz version: 1.2.0 -generated: "2019-08-13T17:33:32.734335398+02:00" +generated: "2019-12-17T12:58:49.474719294+01:00" diff --git a/charts/postgres-operator/postgres-operator-1.3.0.tgz b/charts/postgres-operator/postgres-operator-1.3.0.tgz new file mode 100644 index 000000000..460fed532 Binary files /dev/null and b/charts/postgres-operator/postgres-operator-1.3.0.tgz differ diff --git a/charts/postgres-operator/templates/clusterrole.yaml b/charts/postgres-operator/templates/clusterrole.yaml index a00ea3ab7..f8550a539 100644 --- a/charts/postgres-operator/templates/clusterrole.yaml +++ b/charts/postgres-operator/templates/clusterrole.yaml @@ -87,6 +87,12 @@ rules: - list - watch - patch +- apiGroups: + - "" + resources: + - pods/exec + verbs: + - create - apiGroups: - "" resources: diff --git a/charts/postgres-operator/templates/crds.yaml b/charts/postgres-operator/templates/crds.yaml new file mode 100644 index 000000000..733830014 --- /dev/null +++ b/charts/postgres-operator/templates/crds.yaml @@ -0,0 +1,6 @@ +{{ if .Values.crd.create }} +{{- range $path, $bytes := .Files.Glob "crds/*.yaml" }} +{{ $.Files.Get $path }} +--- +{{- end }} +{{- end }} diff --git a/charts/postgres-operator/templates/customrresourcedefinition.yaml b/charts/postgres-operator/templates/customrresourcedefinition.yaml deleted file mode 100644 index 1c6f1564e..000000000 --- a/charts/postgres-operator/templates/customrresourcedefinition.yaml +++ /dev/null @@ -1,101 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: postgresqls.acid.zalan.do - labels: - app.kubernetes.io/name: {{ template "postgres-operator.name" . }} - helm.sh/chart: {{ template "postgres-operator.chart" . }} - app.kubernetes.io/managed-by: {{ .Release.Service }} - app.kubernetes.io/instance: {{ .Release.Name }} - annotations: - "helm.sh/hook": crd-install -spec: - group: acid.zalan.do - names: - kind: postgresql - listKind: postgresqlList - plural: postgresqls - singular: postgresql - shortNames: - - pg - additionalPrinterColumns: - - name: Team - type: string - description: Team responsible for Postgres CLuster - JSONPath: .spec.teamId - - name: Version - type: string - description: PostgreSQL version - JSONPath: .spec.postgresql.version - - name: Pods - type: integer - description: Number of Pods per Postgres cluster - JSONPath: .spec.numberOfInstances - - name: Volume - type: string - description: Size of the bound volume - JSONPath: .spec.volume.size - - name: CPU-Request - type: string - description: Requested CPU for Postgres containers - JSONPath: .spec.resources.requests.cpu - - name: Memory-Request - type: string - description: Requested memory for Postgres containers - JSONPath: .spec.resources.requests.memory - - name: Age - type: date - JSONPath: .metadata.creationTimestamp - - name: Status - type: string - description: Current sync status of postgresql resource - JSONPath: .status.PostgresClusterStatus - scope: Namespaced - subresources: - status: {} - version: v1 ---- -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: operatorconfigurations.acid.zalan.do - labels: - app.kubernetes.io/name: {{ template "postgres-operator.name" . }} - helm.sh/chart: {{ template "postgres-operator.chart" . }} - app.kubernetes.io/managed-by: {{ .Release.Service }} - app.kubernetes.io/instance: {{ .Release.Name }} - annotations: - "helm.sh/hook": crd-install -spec: - group: acid.zalan.do - names: - kind: OperatorConfiguration - listKind: OperatorConfigurationList - plural: operatorconfigurations - singular: operatorconfiguration - shortNames: - - opconfig - additionalPrinterColumns: - - name: Image - type: string - description: Spilo image to be used for Pods - JSONPath: .configuration.docker_image - - name: Cluster-Label - type: string - description: Label for K8s resources created by operator - JSONPath: .configuration.kubernetes.cluster_name_label - - name: Service-Account - type: string - description: Name of service account to be used - JSONPath: .configuration.kubernetes.pod_service_account_name - - name: Min-Instances - type: integer - description: Minimum number of instances per Postgres cluster - JSONPath: .configuration.min_instances - - name: Age - type: date - JSONPath: .metadata.creationTimestamp - scope: Namespaced - subresources: - status: {} - version: v1 diff --git a/charts/postgres-operator/templates/operatorconfiguration.yaml b/charts/postgres-operator/templates/operatorconfiguration.yaml index c6e9f78b7..6a301c1fb 100644 --- a/charts/postgres-operator/templates/operatorconfiguration.yaml +++ b/charts/postgres-operator/templates/operatorconfiguration.yaml @@ -14,7 +14,7 @@ configuration: {{ toYaml .Values.configUsers | indent 4 }} kubernetes: oauth_token_secret_name: {{ template "postgres-operator.fullname" . }} - pod_service_account_name: operator + pod_service_account_name: {{ include "postgres-operator.serviceAccountName" . }} {{ toYaml .Values.configKubernetes | indent 4 }} postgres_pod_resources: {{ toYaml .Values.configPostgresPodResources | indent 4 }} diff --git a/charts/postgres-operator/values-crd.yaml b/charts/postgres-operator/values-crd.yaml index 1ecd8915b..594533747 100644 --- a/charts/postgres-operator/values-crd.yaml +++ b/charts/postgres-operator/values-crd.yaml @@ -1,7 +1,7 @@ image: registry: registry.opensource.zalan.do repository: acid/postgres-operator - tag: v1.2.0 + tag: v1.3.0 pullPolicy: "IfNotPresent" # Optionally specify an array of imagePullSecrets. @@ -17,12 +17,14 @@ configTarget: "OperatorConfigurationCRD" # general top-level configuration parameters configGeneral: + # choose if deployment creates/updates CRDs with OpenAPIV3Validation + enable_crd_validation: true # start any new database pod without limitations on shm memory enable_shm_volume: true # etcd connection string for Patroni. Empty uses K8s-native DCS. etcd_host: "" # Spilo docker image - docker_image: registry.opensource.zalan.do/acid/spilo-11:1.5-p9 + docker_image: registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p16 # max number of instances in Postgres cluster. -1 = no limit min_instances: -1 # min number of instances in Postgres cluster. -1 = no limit @@ -53,26 +55,36 @@ configKubernetes: cluster_domain: cluster.local # additional labels assigned to the cluster objects cluster_labels: - application: spilo + application: spilo # label assigned to Kubernetes objects created by the operator cluster_name_label: cluster-name + # additional annotations to add to every database pod + # custom_pod_annotations: + # keya: valuea + # keyb: valueb + + # enables initContainers to run actions before Spilo is started + enable_init_containers: true # toggles pod anti affinity on the Postgres pods enable_pod_antiaffinity: false # toggles PDB to set to MinAvailabe 0 or 1 enable_pod_disruption_budget: true + # enables sidecar containers to run alongside Spilo in the same pod + enable_sidecars: true # name of the secret containing infrastructure roles names and passwords # infrastructure_roles_secret_name: postgresql-infrastructure-roles # list of labels that can be inherited from the cluster manifest # inherited_labels: # - application - # - app + # - environment # timeout for successful migration of master pods from unschedulable node # master_pod_move_timeout: 20m # set of labels that a running and active node should possess to be considered ready - # node_readiness_label: "" + # node_readiness_label: + # status: ready # name of the secret containing the OAuth2 token to pass to the teams API # oauth_token_secret_name: postgresql-operator @@ -136,7 +148,7 @@ configLoadBalancer: # keya: valuea # toggles service type load balancer pointing to the master pod of the cluster - enable_master_load_balancer: true + enable_master_load_balancer: false # toggles service type load balancer pointing to the replica pod of the cluster enable_replica_load_balancer: false # defines the DNS name string template for the master load balancer cluster @@ -182,12 +194,20 @@ configAwsOrGcp: # configure K8s cron job managed by the operator configLogicalBackup: - # backup schedule in the cron format - logical_backup_schedule: "30 00 * * *" # image for pods of the logical backup job (example runs pg_dumpall) logical_backup_docker_image: "registry.opensource.zalan.do/acid/logical-backup" + # S3 Access Key ID + logical_backup_s3_access_key_id: "" # S3 bucket to store backup results logical_backup_s3_bucket: "my-bucket-url" + # S3 endpoint url when not using AWS + logical_backup_s3_endpoint: "" + # S3 Secret Access Key + logical_backup_s3_secret_access_key: "" + # S3 server side encription + logical_backup_s3_sse: "AES256" + # backup schedule in the cron format + logical_backup_schedule: "30 00 * * *" # automate creation of human users with teams API service configTeamsApi: @@ -204,7 +224,8 @@ configTeamsApi: # operator will add all team member roles to this group and add a pg_hba line pam_role_name: zalandos # List of teams which members need the superuser role in each Postgres cluster - # postgres_superuser_teams: "postgres_superusers" + # postgres_superuser_teams: + # - postgres_superusers # List of roles that cannot be overwritten by an application, team or infrastructure role protected_role_names: @@ -218,7 +239,7 @@ configTeamsApi: # teams_api_url: http://fake-teams-api.default.svc.cluster.local # Scalyr is a log management tool that Zalando uses as a sidecar -scalyr: +configScalyr: # API key for the Scalyr sidecar # scalyr_api_key: "" @@ -238,14 +259,16 @@ rbac: # Specifies whether RBAC resources should be created create: true +crd: + # Specifies whether custom resource definitions should be created + create: true + serviceAccount: # Specifies whether a ServiceAccount should be created create: true # The name of the ServiceAccount to use. # If not set and create is true, a name is generated using the fullname template - # When relying solely on the OperatorConfiguration CRD, this value has to be "operator" - # Otherwise, the operator tries to use the "default" service account which is forbidden - name: operator + name: priorityClassName: "" diff --git a/charts/postgres-operator/values.yaml b/charts/postgres-operator/values.yaml index b7ec6b21f..ab0785a0c 100644 --- a/charts/postgres-operator/values.yaml +++ b/charts/postgres-operator/values.yaml @@ -1,7 +1,7 @@ image: registry: registry.opensource.zalan.do repository: acid/postgres-operator - tag: v1.2.0 + tag: v1.3.0 pullPolicy: "IfNotPresent" # Optionally specify an array of imagePullSecrets. @@ -17,12 +17,14 @@ configTarget: "ConfigMap" # general configuration parameters configGeneral: + # choose if deployment creates/updates CRDs with OpenAPIV3Validation + enable_crd_validation: "true" # start any new database pod without limitations on shm memory enable_shm_volume: "true" # etcd connection string for Patroni. Empty uses K8s-native DCS. etcd_host: "" # Spilo docker image - docker_image: registry.opensource.zalan.do/acid/spilo-11:1.5-p9 + docker_image: registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p16 # max number of instances in Postgres cluster. -1 = no limit min_instances: "-1" # min number of instances in Postgres cluster. -1 = no limit @@ -54,15 +56,22 @@ configKubernetes: cluster_labels: application:spilo # label assigned to Kubernetes objects created by the operator cluster_name_label: version + # annotations attached to each database pod + # custom_pod_annotations: "keya:valuea,keyb:valueb" + + # enables initContainers to run actions before Spilo is started + enable_init_containers: "true" # toggles pod anti affinity on the Postgres pods enable_pod_antiaffinity: "false" # toggles PDB to set to MinAvailabe 0 or 1 enable_pod_disruption_budget: "true" + # enables sidecar containers to run alongside Spilo in the same pod + enable_sidecars: "true" # name of the secret containing infrastructure roles names and passwords # infrastructure_roles_secret_name: postgresql-infrastructure-roles # list of labels that can be inherited from the cluster manifest - # inherited_labels: "" + # inherited_labels: application,environment # timeout for successful migration of master pods from unschedulable node # master_pod_move_timeout: 20m @@ -127,17 +136,16 @@ configLoadBalancer: # DNS zone for cluster DNS name when load balancer is configured for cluster db_hosted_zone: db.example.com # annotations to apply to service when load balancing is enabled - # custom_service_annotations: - # "keyx:valuez,keya:valuea" + # custom_service_annotations: "keyx:valuez,keya:valuea" # toggles service type load balancer pointing to the master pod of the cluster - enable_master_load_balancer: "true" + enable_master_load_balancer: "false" # toggles service type load balancer pointing to the replica pod of the cluster enable_replica_load_balancer: "false" # defines the DNS name string template for the master load balancer cluster - master_dns_name_format: '{cluster}.{team}.staging.{hostedzone}' + master_dns_name_format: '{cluster}.{team}.{hostedzone}' # defines the DNS name string template for the replica load balancer cluster - replica_dns_name_format: '{cluster}-repl.{team}.staging.{hostedzone}' + replica_dns_name_format: '{cluster}-repl.{team}.{hostedzone}' # options to aid debugging of the operator itself configDebug: @@ -177,12 +185,20 @@ configAwsOrGcp: # configure K8s cron job managed by the operator configLogicalBackup: - # backup schedule in the cron format - logical_backup_schedule: "30 00 * * *" # image for pods of the logical backup job (example runs pg_dumpall) logical_backup_docker_image: "registry.opensource.zalan.do/acid/logical-backup" + # S3 Access Key ID + logical_backup_s3_access_key_id: "" # S3 bucket to store backup results logical_backup_s3_bucket: "my-bucket-url" + # S3 endpoint url when not using AWS + logical_backup_s3_endpoint: "" + # S3 Secret Access Key + logical_backup_s3_secret_access_key: "" + # S3 server side encription + logical_backup_s3_sse: "AES256" + # backup schedule in the cron format + logical_backup_schedule: "30 00 * * *" # automate creation of human users with teams API service configTeamsApi: @@ -219,6 +235,10 @@ rbac: # Specifies whether RBAC resources should be created create: true +crd: + # Specifies whether custom resource definitions should be created + create: true + serviceAccount: # Specifies whether a ServiceAccount should be created create: true diff --git a/delivery.yaml b/delivery.yaml index 8c4db2b90..be35d3e27 100644 --- a/delivery.yaml +++ b/delivery.yaml @@ -2,9 +2,6 @@ version: "2017-09-20" pipeline: - id: build-postgres-operator type: script - env: - GOPATH: /root/go - OPERATOR_TOP_DIR: /root/go/src/github.com/zalando commands: - desc: 'Update' cmd: | @@ -20,10 +17,6 @@ pipeline: mv go /usr/local ln -s /usr/local/go/bin/go /usr/bin/go go version - - desc: 'Symlink sources into the GOPATH' - cmd: | - mkdir -p $OPERATOR_TOP_DIR - ln -s $(pwd) $OPERATOR_TOP_DIR/postgres-operator - desc: 'Build docker image' cmd: | export PATH=$PATH:$HOME/go/bin @@ -35,15 +28,13 @@ pipeline: IMAGE=registry-write.opensource.zalan.do/acid/postgres-operator-test fi export IMAGE - make tools deps docker + make deps docker - desc: 'Run unit tests' cmd: | export PATH=$PATH:$HOME/go/bin - cd $OPERATOR_TOP_DIR/postgres-operator go test ./... - desc: 'Run e2e tests' cmd: | - cd $OPERATOR_TOP_DIR/postgres-operator make e2e - desc: 'Push docker image' cmd: | diff --git a/docker/logical-backup/dump.sh b/docker/logical-backup/dump.sh index dcbd7d334..78217322b 100755 --- a/docker/logical-backup/dump.sh +++ b/docker/logical-backup/dump.sh @@ -38,24 +38,26 @@ function aws_upload { # NB: $LOGICAL_BACKUP_S3_BUCKET_SCOPE_SUFFIX already contains the leading "/" when set by the Postgres Operator PATH_TO_BACKUP=s3://$LOGICAL_BACKUP_S3_BUCKET"/spilo/"$SCOPE$LOGICAL_BACKUP_S3_BUCKET_SCOPE_SUFFIX"/logical_backups/"$(date +%s).sql.gz - if [ -z "$EXPECTED_SIZE" ]; then - aws s3 cp - "$PATH_TO_BACKUP" --debug --sse="AES256" - else - aws s3 cp - "$PATH_TO_BACKUP" --debug --expected-size "$EXPECTED_SIZE" --sse="AES256" - fi; + args=() + + [[ ! -z "$EXPECTED_SIZE" ]] && args+=("--expected-size=$EXPECTED_SIZE") + [[ ! -z "$LOGICAL_BACKUP_S3_ENDPOINT" ]] && args+=("--endpoint-url=$LOGICAL_BACKUP_S3_ENDPOINT") + [[ ! "$LOGICAL_BACKUP_S3_SSE" == "" ]] && args+=("--sse=$LOGICAL_BACKUP_S3_SSE") + + aws s3 cp - "$PATH_TO_BACKUP" "${args[@]//\'/}" --debug } function get_pods { declare -r SELECTOR="$1" - curl "${K8S_API_URL}/namespaces/${POD_NAMESPACE}/pods?$SELECTOR" \ - --cacert $CERT \ + curl "${K8S_API_URL}/namespaces/${POD_NAMESPACE}/pods?$SELECTOR" \ + --cacert $CERT \ -H "Authorization: Bearer ${TOKEN}" | jq .items[].status.podIP -r } function get_current_pod { curl "${K8S_API_URL}/namespaces/${POD_NAMESPACE}/pods?fieldSelector=metadata.name%3D${HOSTNAME}" \ - --cacert $CERT \ + --cacert $CERT \ -H "Authorization: Bearer ${TOKEN}" } @@ -66,15 +68,15 @@ declare -a search_strategy=( ) function list_all_replica_pods_current_node { - get_pods "labelSelector=version%3D${SCOPE},spilo-role%3Dreplica&fieldSelector=spec.nodeName%3D${CURRENT_NODENAME}" | head -n 1 + get_pods "labelSelector=${CLUSTER_NAME_LABEL}%3D${SCOPE},spilo-role%3Dreplica&fieldSelector=spec.nodeName%3D${CURRENT_NODENAME}" | head -n 1 } function list_all_replica_pods_any_node { - get_pods "labelSelector=version%3D${SCOPE},spilo-role%3Dreplica" | head -n 1 + get_pods "labelSelector=${CLUSTER_NAME_LABEL}%3D${SCOPE},spilo-role%3Dreplica" | head -n 1 } function get_master_pod { - get_pods "labelSelector=version%3D${SCOPE},spilo-role%3Dmaster" | head -n 1 + get_pods "labelSelector=${CLUSTER_NAME_LABEL}%3D${SCOPE},spilo-role%3Dmaster" | head -n 1 } CURRENT_NODENAME=$(get_current_pod | jq .items[].spec.nodeName --raw-output) diff --git a/docs/administrator.md b/docs/administrator.md index 5eaf3ff71..5b8769edb 100644 --- a/docs/administrator.md +++ b/docs/administrator.md @@ -3,6 +3,50 @@ Learn how to configure and manage the Postgres Operator in your Kubernetes (K8s) environment. +## Minor and major version upgrade + +Minor version upgrades for PostgreSQL are handled via updating the Spilo Docker +image. The operator will carry out a rolling update of Pods which includes a +switchover (planned failover) of the master to the Pod with new minor version. +The switch should usually take less than 5 seconds, still clients have to +reconnect. + +Major version upgrades are supported via [cloning](user.md#clone-directly). The +new cluster manifest must have a higher `version` string than the source cluster +and will be created from a basebackup. Depending of the cluster size, downtime +in this case can be significant as writes to the database should be stopped and +all WAL files should be archived first before cloning is started. + +Note, that simply changing the version string in the `postgresql` manifest does +not work at present and leads to errors. Neither Patroni nor Postgres Operator +can do in place `pg_upgrade`. Still, it can be executed manually in the Postgres +container, which is tricky (i.e. systems need to be stopped, replicas have to be +synced) but of course faster than cloning. + +## CRD Validation + +[CustomResourceDefinitions](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#customresourcedefinitions) +will be registered with schema validation by default when the operator is +deployed. The `OperatorConfiguration` CRD will only get created if the +`POSTGRES_OPERATOR_CONFIGURATION_OBJECT` [environment variable](../manifests/postgres-operator.yaml#L36) +in the deployment yaml is set and not empty. + +When submitting manifests of [`postgresql`](../manifests/postgresql.crd.yaml) or +[`OperatorConfiguration`](../manifests/operatorconfiguration.crd.yaml) custom +resources with kubectl, validation can be bypassed with `--validate=false`. The +operator can also be configured to not register CRDs with validation on `ADD` or +`UPDATE` events. Running instances are not affected when enabling the validation +afterwards unless the manifests is not changed then. Note, that the provided CRD +manifests contain the validation for users to understand what schema is +enforced. + +Once the validation is enabled it can only be disabled manually by editing or +patching the CRD manifest: + +```bash +zk8 patch crd postgresqls.acid.zalan.do -p '{"spec":{"validation": null}}' +``` + ## Namespaces ### Select the namespace to deploy to @@ -32,7 +76,7 @@ By default, the operator watches the namespace it is deployed to. You can change this by setting the `WATCHED_NAMESPACE` var in the `env` section of the [operator deployment](../manifests/postgres-operator.yaml) manifest or by altering the `watched_namespace` field in the operator -[ConfigMap](../manifests/configmap.yaml#L79). +[configuration](../manifests/postgresql-operator-default-configuration.yaml#L49). In the case both are set, the env var takes the precedence. To make the operator listen to all namespaces, explicitly set the field/env var to "`*`". @@ -71,8 +115,6 @@ is used by the operator to connect to the clusters after creation. ## Role-based access control for the operator -### Service account and cluster roles - The manifest [`operator-service-account-rbac.yaml`](../manifests/operator-service-account-rbac.yaml) defines the service account, cluster roles and bindings needed for the operator to function under access control restrictions. To deploy the operator with this @@ -85,6 +127,8 @@ kubectl create -f manifests/postgres-operator.yaml kubectl create -f manifests/minimal-postgres-manifest.yaml ``` +### Service account and cluster roles + Note that the service account is named `zalando-postgres-operator`. You may have to change the `service_account_name` in the operator ConfigMap and `serviceAccountName` in the `postgres-operator` deployment appropriately. This @@ -92,12 +136,6 @@ is done intentionally to avoid breaking those setups that already work with the default `operator` account. In the future the operator should ideally be run under the `zalando-postgres-operator` service account. -The service account defined in `operator-service-account-rbac.yaml` acquires -some privileges not used by the operator (i.e. we only need `list` and `watch` -on `configmaps` resources). This is also done intentionally to avoid breaking -things if someone decides to configure the same service account in the -operator's ConfigMap to run Postgres clusters. - ### Give K8s users access to create/list `postgresqls` By default `postgresql` custom resources can only be listed and changed by @@ -115,7 +153,7 @@ that are aggregated into the K8s [default roles](https://kubernetes.io/docs/refe To ensure Postgres pods are running on nodes without any other application pods, you can use [taints and tolerations](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/) -and configure the required toleration in the operator ConfigMap. +and configure the required toleration in the operator configuration. As an example you can set following node taint: @@ -133,7 +171,20 @@ metadata: name: postgres-operator data: toleration: "key:postgres,operator:Exists,effect:NoSchedule" - ... +``` + +For an OperatorConfiguration resource the toleration should be defined like +this: + +```yaml +apiVersion: "acid.zalan.do/v1" +kind: OperatorConfiguration +metadata: + name: postgresql-configuration +configuration: + kubernetes: + toleration: + postgres: "key:postgres,operator:Exists,effect:NoSchedule" ``` Note that the K8s version 1.13 brings [taint-based eviction](https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/#taint-based-evictions) @@ -148,7 +199,7 @@ completely, specify the toleration by leaving out the `tolerationSeconds` value To ensure Postgres pods are running on different topologies, you can use [pod anti affinity](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/) -and configure the required topology in the operator ConfigMap. +and configure the required topology in the operator configuration. Enable pod anti affinity by adding following line to the operator ConfigMap: @@ -161,21 +212,22 @@ data: enable_pod_antiaffinity: "true" ``` -By default the topology key for the pod anti affinity is set to -`kubernetes.io/hostname`, you can set another topology key e.g. -`failure-domain.beta.kubernetes.io/zone` by adding following line to the -operator ConfigMap, see [built-in node labels](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#interlude-built-in-node-labels) for available topology keys: +Likewise, when using an OperatorConfiguration resource add: ```yaml -apiVersion: v1 -kind: ConfigMap +apiVersion: "acid.zalan.do/v1" +kind: OperatorConfiguration metadata: - name: postgres-operator -data: - enable_pod_antiaffinity: "true" - pod_antiaffinity_topology_key: "failure-domain.beta.kubernetes.io/zone" + name: postgresql-configuration +configuration: + kubernetes: + enable_pod_antiaffinity: true ``` +By default the topology key for the pod anti affinity is set to +`kubernetes.io/hostname`, you can set another topology key e.g. +`failure-domain.beta.kubernetes.io/zone`. See [built-in node labels](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#interlude-built-in-node-labels) for available topology keys. + ## Pod Disruption Budget By default the operator uses a PodDisruptionBudget (PDB) to protect the cluster @@ -184,6 +236,7 @@ parameter of the PDB is set to `1` which prevents killing masters in single-node clusters and/or the last remaining running instance in a multi-node cluster. The PDB is only relaxed in two scenarios: + * If a cluster is scaled down to `0` instances (e.g. for draining nodes) * If the PDB is disabled in the configuration (`enable_pod_disruption_budget`) @@ -200,6 +253,17 @@ Postgres cluster, in order to identify its child objects. The typical use case is to add labels that identifies the `Pods` created by the operator, in order to implement fine-controlled `NetworkPolicies`. +**postgres-operator ConfigMap** + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: postgres-operator +data: + inherited_labels: application,environment +``` + **OperatorConfiguration** ```yaml @@ -212,7 +276,6 @@ configuration: inherited_labels: - application - environment -... ``` **cluster manifest** @@ -226,7 +289,7 @@ metadata: application: my-app environment: demo spec: -... + ... ``` **network policy** @@ -241,7 +304,6 @@ spec: matchLabels: application: my-app environment: demo -... ``` @@ -264,7 +326,19 @@ metadata: data: # referencing config map with custom settings pod_environment_configmap: postgres-pod-config - ... +``` + +**OperatorConfiguration** + +```yaml +apiVersion: "acid.zalan.do/v1" +kind: OperatorConfiguration +metadata: + name: postgresql-operator-configuration +configuration: + kubernetes: + # referencing config map with custom settings + pod_environment_configmap: postgres-pod-config ``` **referenced ConfigMap `postgres-pod-config`** @@ -299,7 +373,7 @@ services: one for the master pod and one for replica pods. To expose these services to an outer network, one can attach load balancers to them by setting `enableMasterLoadBalancer` and/or `enableReplicaLoadBalancer` to `true` in the cluster manifest. In the case any of these variables are omitted from the -manifest, the operator configmap's settings `enable_master_load_balancer` and +manifest, the operator configuration settings `enable_master_load_balancer` and `enable_replica_load_balancer` apply. Note that the operator settings affect all Postgresql services running in all namespaces watched by the operator. @@ -345,12 +419,12 @@ external systems but defined for an individual Postgres cluster in its manifest. A typical example is a role for connections from an application that uses the database. -* **Human users** originate from the Teams API that returns a list of the team -members given a team id. The operator differentiates between (a) product teams -that own a particular Postgres cluster and are granted admin rights to maintain -it, and (b) Postgres superuser teams that get the superuser access to all -Postgres databases running in a K8s cluster for the purposes of maintaining and -troubleshooting. +* **Human users** originate from the [Teams API](user.md#teams-api-roles) that +returns a list of the team members given a team id. The operator differentiates +between (a) product teams that own a particular Postgres cluster and are granted +admin rights to maintain it, and (b) Postgres superuser teams that get the +superuser access to all Postgres databases running in a K8s cluster for the +purposes of maintaining and troubleshooting. ## Understanding rolling update of Spilo pods @@ -375,7 +449,7 @@ manifest. Notes: backup via `pg_dumpall` and upload of compressed and encrypted results to an S3 bucket; the default image ``registry.opensource.zalan.do/acid/logical-backup`` is the same image built with the Zalando-internal CI pipeline. `pg_dumpall` -requires a `superuser` access to a DB and runs on the replica when possible. +requires a `superuser` access to a DB and runs on the replica when possible. 2. Due to the [limitation of K8s cron jobs](https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#cron-job-limitations) it is highly advisable to set up additional monitoring for this feature; such @@ -414,7 +488,7 @@ A secret can be pre-provisioned in different ways: With the v1.2 release the Postgres Operator is shipped with a browser-based configuration user interface (UI) that simplifies managing Postgres clusters -with the operator. The UI runs with Node.js and comes with it's own docker +with the operator. The UI runs with Node.js and comes with it's own Docker image. Run NPM to continuously compile `tags/js` code. Basically, it creates an @@ -426,14 +500,14 @@ Run NPM to continuously compile `tags/js` code. Basically, it creates an To build the Docker image open a shell and change to the `ui` folder. Then run: -``` +```bash docker build -t registry.opensource.zalan.do/acid/postgres-operator-ui:v1.2.0 . ``` Apply all manifests for the `ui/manifests` folder to deploy the Postgres Operator UI on K8s. For local tests you don't need the Ingress resource. -``` +```bash kubectl apply -f ui/manifests ``` @@ -443,6 +517,6 @@ to the K8s and Postgres Operator REST API. You can use the provided `run_local.sh` script for this. Make sure it uses the correct URL to your K8s API server, e.g. for minikube it would be `https://192.168.99.100:8443`. -``` +```bash ./run_local.sh ``` diff --git a/docs/developer.md b/docs/developer.md index a94edefc0..6e0fc33c8 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -27,23 +27,20 @@ git clone https://github.com/zalando/postgres-operator.git ## Building the operator -You need Glide to fetch all dependencies. Install it with: - -```bash -make tools -``` - -Next, install dependencies with glide by issuing: +We use [Go Modules](https://github.com/golang/go/wiki/Modules) for handling +dependencies. When using Go below v1.13 you need to explicitly enable Go modules +by setting the `GO111MODULE` environment variable to `on`. The make targets do +this for you, so simply run ```bash make deps ``` This would take a while to complete. You have to redo `make deps` every time -you dependencies list changes, i.e. after adding a new library dependency. +your dependencies list changes, i.e. after adding a new library dependency. Build the operator with the `make docker` command. You may define the TAG -variable to assign an explicit tag to your docker image and the IMAGE to set +variable to assign an explicit tag to your Docker image and the IMAGE to set the image name. By default, the tag is computed with `git describe --tags --always --dirty` and the image is `registry.opensource.zalan.do/acid/postgres-operator` @@ -63,10 +60,10 @@ The binary will be placed into the build directory. ## Deploying self build image -The fastest way to run and test your docker image locally is to reuse the docker -from [minikube](https://github.com/kubernetes/minikube/releases) or use the -`load docker-image` from [kind](https://kind.sigs.k8s.io/). The following steps -will get you the docker image built and deployed. +The fastest way to run and test your Docker image locally is to reuse the Docker +environment from [minikube](https://github.com/kubernetes/minikube/releases) +or use the `load docker-image` from [kind](https://kind.sigs.k8s.io/). The +following steps will get you the Docker image built and deployed. ```bash # minikube @@ -165,7 +162,7 @@ The operator also supports pprof endpoints listed at the * /debug/pprof/trace It's possible to attach a debugger to troubleshoot postgres-operator inside a -docker container. It's possible with [gdb](https://www.gnu.org/software/gdb/) +Docker container. It's possible with [gdb](https://www.gnu.org/software/gdb/) and [delve](https://github.com/derekparker/delve). Since the latter one is a specialized debugger for Go, we will use it as an example. To use it you need: @@ -214,14 +211,7 @@ dlv connect 127.0.0.1:DLV_PORT To run all unit tests, you can simply do: ```bash -go test ./... -``` - -For go 1.9 `vendor` directory would be excluded automatically. For previous -versions you can exclude it manually: - -```bash -go test $(glide novendor) +go test ./pkg/... ``` In case if you need to debug your unit test, it's possible to use delve: @@ -294,6 +284,7 @@ manifest files: Postgres manifest parameters are defined in the [api package](../pkg/apis/acid.zalan.do/v1/postgresql_type.go). The operator behavior has to be implemented at least in [k8sres.go](../pkg/cluster/k8sres.go). +Validation of CRD parameters is controlled in [crd.go](../pkg/apis/acid.zalan.do/v1/crds.go). Please, reflect your changes in tests, for example in: * [config_test.go](../pkg/util/config/config_test.go) * [k8sres_test.go](../pkg/cluster/k8sres_test.go) @@ -304,6 +295,7 @@ Please, reflect your changes in tests, for example in: For the CRD-based configuration, please update the following files: * the default [OperatorConfiguration](../manifests/postgresql-operator-default-configuration.yaml) * the Helm chart's [values-crd file](../charts/postgres-operator/values.yaml) +* the CRD's [validation](../manifests/operatorconfiguration.crd.yaml) Reflect the changes in the ConfigMap configuration as well (note that numeric and boolean parameters have to use double quotes here): diff --git a/docs/index.md b/docs/index.md index c0e78ac32..87b08deb2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,7 +13,7 @@ manages PostgreSQL clusters on Kubernetes (K8s): 2. The operator also watches updates to [its own configuration](../manifests/configmap.yaml) and alters running Postgres clusters if necessary. For instance, if the - docker image in a pod is changed, the operator carries out the rolling + Docker image in a pod is changed, the operator carries out the rolling update, which means it re-spawns pods of each managed StatefulSet one-by-one with the new Docker image. diff --git a/docs/quickstart.md b/docs/quickstart.md index b8b97c94e..8cc5bc0c0 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -55,15 +55,15 @@ kubectl create -f manifests/postgres-operator.yaml # deployment ``` There is a [Kustomization](https://github.com/kubernetes-sigs/kustomize) -manifest that [combines the mentioned resources](../manifests/kustomization.yaml) - -it can be used with kubectl 1.14 or newer as easy as: +manifest that [combines the mentioned resources](../manifests/kustomization.yaml) +(except for the CRD) - it can be used with kubectl 1.14 or newer as easy as: ```bash kubectl apply -k github.com/zalando/postgres-operator/manifests ``` For convenience, we have automated starting the operator with minikube using the -`run_operator_locally` script. It applies the [`acid-minimal-cluster`](../manifests/minimal-postgres-manifest). +`run_operator_locally` script. It applies the [`acid-minimal-cluster`](../manifests/minimal-postgres-manifest.yaml). manifest. ```bash @@ -73,22 +73,23 @@ manifest. ### Helm chart Alternatively, the operator can be installed by using the provided [Helm](https://helm.sh/) -chart which saves you the manual steps. Therefore, install the helm CLI on your -machine. After initializing helm (and its server component Tiller) in your local -cluster you can install the operator chart. You can define a release name that -is prepended to the operator resource's names. - -Use `--name zalando` to match with the default service account name as older -operator versions do not support custom names for service accounts. To use -CRD-based configuration you need to specify the [values-crd yaml file](../charts/values-crd.yaml). +chart which saves you the manual steps. Clone this repo and change directory to +the repo root. With Helm v3 installed you should be able to run: ```bash -# 1) initialize helm -helm init -# 2) install postgres-operator chart -helm install --name zalando ./charts/postgres-operator +helm install postgres-operator ./charts/postgres-operator ``` +To use CRD-based configuration you need to specify the [values-crd yaml file](../charts/postgres-operator/values-crd.yaml). + +```bash +helm install postgres-operator ./charts/postgres-operator -f ./charts/postgres-operator/values-crd.yaml +``` + +The chart works with both Helm 2 and Helm 3. The `crd-install` hook from v2 will +be skipped with warning when using v3. Documentation for installing applications +with Helm 2 can be found in the [v2 docs](https://v2.helm.sh/docs/). + ### Operator Lifecycle Manager (OLM) The [Operator Lifecycle Manager (OLM)](https://github.com/operator-framework/operator-lifecycle-manager) @@ -119,15 +120,15 @@ kubectl get pod -l app.kubernetes.io/name=postgres-operator kubectl create -f manifests/minimal-postgres-manifest.yaml ``` -After the cluster manifest is submitted the operator will create Service and -Endpoint resources and a StatefulSet which spins up new Pod(s) given the number -of instances specified in the manifest. All resources are named like the -cluster. The database pods can be identified by their number suffix, starting -from `-0`. They run the [Spilo](https://github.com/zalando/spilo) container -image by Zalando. As for the services and endpoints, there will be one for the -master pod and another one for all the replicas (`-repl` suffix). Check if all -components are coming up. Use the label `application=spilo` to filter and list -the label `spilo-role` to see who is currently the master. +After the cluster manifest is submitted and passed the validation the operator +will create Service and Endpoint resources and a StatefulSet which spins up new +Pod(s) given the number of instances specified in the manifest. All resources +are named like the cluster. The database pods can be identified by their number +suffix, starting from `-0`. They run the [Spilo](https://github.com/zalando/spilo) +container image by Zalando. As for the services and endpoints, there will be one +for the master pod and another one for all the replicas (`-repl` suffix). Check +if all components are coming up. Use the label `application=spilo` to filter and +list the label `spilo-role` to see who is currently the master. ```bash # check the deployed cluster @@ -154,9 +155,12 @@ export PGPORT=$(echo $HOST_PORT | cut -d: -f 2) ``` Retrieve the password from the K8s Secret that is created in your cluster. +Non-encrypted connections are rejected by default, so set the SSL mode to +require: ```bash export PGPASSWORD=$(kubectl get secret postgres.acid-minimal-cluster.credentials -o 'jsonpath={.data.password}' | base64 -d) +export PGSSLMODE=require psql -U postgres ``` diff --git a/docs/reference/cluster_manifest.md b/docs/reference/cluster_manifest.md index 7c2839662..bf6df681b 100644 --- a/docs/reference/cluster_manifest.md +++ b/docs/reference/cluster_manifest.md @@ -4,9 +4,9 @@ Individual Postgres clusters are described by the Kubernetes *cluster manifest* that has the structure defined by the `postgresql` CRD (custom resource definition). The following section describes the structure of the manifest and the purpose of individual keys. You can take a look at the examples of the -[minimal](../manifests/minimal-postgres-manifest.yaml) +[minimal](../../manifests/minimal-postgres-manifest.yaml) and the -[complete](../manifests/complete-postgres-manifest.yaml) +[complete](../../manifests/complete-postgres-manifest.yaml) cluster manifests. When Kubernetes resources, such as memory, CPU or volumes, are configured, @@ -62,7 +62,7 @@ These parameters are grouped directly under the `spec` key in the manifest. field. * **dockerImage** - custom docker image that overrides the **docker_image** operator parameter. + custom Docker image that overrides the **docker_image** operator parameter. It should be a [Spilo](https://github.com/zalando/spilo) image. Optional. * **spiloFSGroup** @@ -118,8 +118,13 @@ These parameters are grouped directly under the `spec` key in the manifest. then the default priority class is taken. The priority class itself must be defined in advance. Optional. +* **podAnnotations** + A map of key value pairs that gets attached as [annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) + to each pod created for the database. + + * **enableShmVolume** - Start a database pod without limitations on shm memory. By default docker + Start a database pod without limitations on shm memory. By default Docker limit `/dev/shm` to `64M` (see e.g. the [docker issue](https://github.com/docker-library/postgres/issues/416), which could be not enough if PostgreSQL uses parallel workers heavily. If this option is @@ -180,19 +185,19 @@ explanation of `ttl` and `loop_wait` parameters. * **ttl** Patroni `ttl` parameter value, optional. The default is set by the Spilo - docker image. Optional. + Docker image. Optional. * **loop_wait** Patroni `loop_wait` parameter value, optional. The default is set by the - Spilo docker image. Optional. + Spilo Docker image. Optional. * **retry_timeout** Patroni `retry_timeout` parameter value, optional. The default is set by the - Spilo docker image. Optional. + Spilo Docker image. Optional. * **maximum_lag_on_failover** Patroni `maximum_lag_on_failover` parameter value, optional. The default is - set by the Spilo docker image. Optional. + set by the Spilo Docker image. Optional. * **slots** permanent replication slots that Patroni preserves after failover by @@ -315,7 +320,7 @@ defined in the sidecar dictionary: name of the sidecar. Required. * **image** - docker image of the sidecar. Required. + Docker image of the sidecar. Required. * **env** a dictionary of environment variables. Use usual Kubernetes definition diff --git a/docs/reference/operator_parameters.md b/docs/reference/operator_parameters.md index 3e21340d5..1055d89b6 100644 --- a/docs/reference/operator_parameters.md +++ b/docs/reference/operator_parameters.md @@ -10,12 +10,12 @@ configuration. maps. String values containing ':' should be enclosed in quotes. The configuration is flat, parameter group names below are not reflected in the configuration structure. There is an - [example](../manifests/configmap.yaml) + [example](../../manifests/configmap.yaml) * CRD-based configuration. The configuration is stored in a custom YAML manifest. The manifest is an instance of the custom resource definition (CRD) called `OperatorConfiguration`. The operator registers this CRD during the - start and uses it for configuration if the [operator deployment manifest](../manifests/postgres-operator.yaml#L36) + start and uses it for configuration if the [operator deployment manifest](../../manifests/postgres-operator.yaml#L36) sets the `POSTGRES_OPERATOR_CONFIGURATION_OBJECT` env variable to a non-empty value. The variable should point to the `postgresql-operator-configuration` object in the operator's namespace. @@ -24,26 +24,25 @@ configuration. simply represented in the usual YAML way. There are no default values built-in in the operator, each parameter that is not supplied in the configuration receives an empty value. In order to create your own configuration just copy - the [default one](../manifests/postgresql-operator-default-configuration.yaml) + the [default one](../../manifests/postgresql-operator-default-configuration.yaml) and change it. To test the CRD-based configuration locally, use the following ```bash + kubectl create -f manifests/operatorconfiguration.crd.yaml # registers the CRD + kubectl create -f manifests/postgresql-operator-default-configuration.yaml + kubectl create -f manifests/operator-service-account-rbac.yaml kubectl create -f manifests/postgres-operator.yaml # set the env var as mentioned above - kubectl create -f manifests/postgresql-operator-default-configuration.yaml + kubectl get operatorconfigurations postgresql-operator-default-configuration -o yaml ``` - Note that the operator first attempts to register the CRD of the - `OperatorConfiguration` and then waits for an instance to be created. In - between these two event the operator pod may be failing since it cannot fetch - the not-yet-existing `OperatorConfiguration` instance. The CRD-based configuration is more powerful than the one based on ConfigMaps and should be used unless there is a compatibility requirement to use an already existing configuration. Even in that case, it should be rather straightforward -to convert the configmap based configuration into the CRD-based one and restart -the operator. The ConfigMaps-based configuration will be deprecated and +to convert the ConfigMap-based configuration into the CRD-based one and restart +the operator. The ConfigMap-based configuration will be deprecated and subsequently removed in future releases. Note that for the CRD-based configuration groups of configuration options below @@ -58,11 +57,11 @@ parameters, those parameters have no effect and are replaced by the `CRD_READY_WAIT_INTERVAL` and `CRD_READY_WAIT_TIMEOUT` environment variables. They will be deprecated and removed in the future. -For the configmap configuration, the [default parameter values](../pkg/util/config/config.go#L14) +For the configmap configuration, the [default parameter values](../../pkg/util/config/config.go#L14) mentioned here are likely to be overwritten in your local operator installation via your local version of the operator configmap. In the case you use the operator CRD, all the CRD defaults are provided in the -[operator's default configuration manifest](../manifests/postgresql-operator-default-configuration.yaml) +[operator's default configuration manifest](../../manifests/postgresql-operator-default-configuration.yaml) Variable names are underscore-separated words. @@ -71,21 +70,26 @@ Variable names are underscore-separated words. Those are top-level keys, containing both leaf keys and groups. +* **enable_crd_validation** + toggles if the operator will create or update CRDs with + [OpenAPI v3 schema validation](https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#validation) + The default is `true`. + * **etcd_host** Etcd connection string for Patroni defined as `host:port`. Not required when Patroni native Kubernetes support is used. The default is empty (use Kubernetes-native DCS). * **docker_image** - Spilo docker image for Postgres instances. For production, don't rely on the + Spilo Docker image for Postgres instances. For production, don't rely on the default image, as it might be not the most up-to-date one. Instead, build your own Spilo image from the [github repository](https://github.com/zalando/spilo). * **sidecar_docker_images** - a map of sidecar names to docker images for the containers to run alongside - Spilo. In case of the name conflict with the definition in the cluster - manifest the cluster-specific one is preferred. + a map of sidecar names to Docker images to run with Spilo. In case of the name + conflict with the definition in the cluster manifest the cluster-specific one + is preferred. * **enable_shm_volume** Instruct operator to start any new database pod without limitations on shm @@ -122,7 +126,7 @@ Those are top-level keys, containing both leaf keys and groups. containers with high memory limits due to the lack of memory on Kubernetes cluster nodes. This affects all containers created by the operator (Postgres, Scalyr sidecar, and other sidecars); to set resources for the operator's own - container, change the [operator deployment manually](../manifests/postgres-operator.yaml#L20). + container, change the [operator deployment manually](../../manifests/postgres-operator.yaml#L20). The default is `false`. ## Postgres users @@ -168,6 +172,11 @@ configuration they are grouped under the `kubernetes` key. Postgres pods are [terminated forcefully](https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods) after this timeout. The default is `5m`. +* **custom_pod_annotations** + This key/value map provides a list of annotations that get attached to each pod + of a database created by the operator. If the annotation key is also provided + by the database definition, the database definition value is used. + * **watched_namespace** The operator watches for Postgres objects in the given namespace. If not specified, the value is taken from the operator namespace. A special `*` @@ -187,6 +196,14 @@ configuration they are grouped under the `kubernetes` key. [admin docs](../administrator.md#pod-disruption-budget) for more information. Default is true. +* **enable_init_containers** + global option to allow for creating init containers to run actions before + Spilo is started. Default is true. + +* **enable_sidecars** + global option to allow for creating sidecar containers to run alongside Spilo + on the same pod. Default is true. + * **secret_name_template** a template for the name of the database user secrets generated by the operator. `{username}` is replaced with name of the secret, `{cluster}` with @@ -436,6 +453,19 @@ grouped under the `logical_backup` key. S3 bucket to store backup results. The bucket has to be present and accessible by Postgres pods. Default: empty. +* **logical_backup_s3_endpoint** + When using non-AWS S3 storage, endpoint can be set as a ENV variable. + +* **logical_backup_s3_sse** + Specify server side encription that S3 storage is using. If empty string + is specified, no argument will be passed to `aws s3` command. Default: "AES256". + +* **logical_backup_s3_access_key_id** + When set, value will be in AWS_ACCESS_KEY_ID env variable. The Default is empty. + +* **logical_backup_s3_secret_access_key** + When set, value will be in AWS_SECRET_ACCESS_KEY env variable. The Default is empty. + ## Debugging the operator Options to aid debugging of the operator itself. Grouped under the `debug` key. diff --git a/docs/user.md b/docs/user.md index ee8e7183c..45f345c87 100644 --- a/docs/user.md +++ b/docs/user.md @@ -13,7 +13,7 @@ kind: postgresql metadata: name: acid-minimal-cluster spec: - teamId: "ACID" + teamId: "acid" volume: size: 1Gi numberOfInstances: 2 @@ -30,7 +30,7 @@ spec: databases: foo: zalando postgresql: - version: "10" + version: "11" ``` Once you cloned the Postgres Operator [repository](https://github.com/zalando/postgres-operator) @@ -40,6 +40,17 @@ you can find this example also in the manifests folder: kubectl create -f manifests/minimal-postgres-manifest.yaml ``` +Make sure, the `spec` section of the manifest contains at least a `teamId`, the +`numberOfInstances` and the `postgresql` object with the `version` specified. +The minimum volume size to run the `postgresql` resource on Elastic Block +Storage (EBS) is `1Gi`. + +Note, that the name of the cluster must start with the `teamId` and `-`. At +Zalando we use team IDs (nicknames) to lower the chance of duplicate cluster +names and colliding entities. The team ID would also be used to query an API to +get all members of a team and create [database roles](#teams-api-roles) for +them. + ## Watch pods being created ```bash @@ -62,10 +73,12 @@ kubectl port-forward $PGMASTER 6432:5432 Open another CLI and connect to the database. Use the generated secret of the `postgres` robot user to connect to our `acid-minimal-cluster` master running -in Minikube: +in Minikube. As non-encrypted connections are rejected by default set the SSL +mode to require: ```bash export PGPASSWORD=$(kubectl get secret postgres.acid-minimal-cluster.credentials -o 'jsonpath={.data.password}' | base64 -d) +export PGSSLMODE=require psql -U postgres -p 6432 ``` @@ -77,8 +90,7 @@ cluster. It covers three use-cases: * `manifest roles`: create application roles specific to the cluster described in the manifest. * `infrastructure roles`: create application roles that should be automatically -created on every - cluster managed by the operator. +created on every cluster managed by the operator. * `teams API roles`: automatically create users for every member of the team owning the database cluster. @@ -128,9 +140,9 @@ The infrastructure roles secret is specified by the `infrastructure_roles_secret parameter. The role definition looks like this (values are base64 encoded): ```yaml - user1: ZGJ1c2Vy - password1: c2VjcmV0 - inrole1: b3BlcmF0b3I= +user1: ZGJ1c2Vy +password1: c2VjcmV0 +inrole1: b3BlcmF0b3I= ``` The block above describes the infrastructure role 'dbuser' with password @@ -151,19 +163,19 @@ secret and a ConfigMap. The ConfigMap must have the same name as the secret. The secret should contain an entry with 'rolename:rolepassword' for each role. ```yaml - dbuser: c2VjcmV0 +dbuser: c2VjcmV0 ``` And the role description for that user should be specified in the ConfigMap. ```yaml - data: - dbuser: | - inrole: [operator, admin] # following roles will be assigned to the new user - user_flags: - - createdb - db_parameters: # db parameters, applied for this particular user - log_statement: all +data: + dbuser: | + inrole: [operator, admin] # following roles will be assigned to the new user + user_flags: + - createdb + db_parameters: # db parameters, applied for this particular user + log_statement: all ``` One can allow membership in multiple roles via the `inrole` array parameter, @@ -182,6 +194,50 @@ See [infrastructure roles secret](../manifests/infrastructure-roles.yaml) and [infrastructure roles configmap](../manifests/infrastructure-roles-configmap.yaml) for the examples. +### Teams API roles + +These roles are meant for database activity of human users. It's possible to +configure the operator to automatically create database roles for lets say all +employees of one team. They are not listed in the manifest and there are no K8s +secrets created for them. Instead they would use an OAuth2 token to connect. To +get all members of the team the operator queries a defined API endpoint that +returns usernames. A minimal Teams API should work like this: + +``` +/.../ -> ["name","anothername"] +``` + +A ["fake" Teams API](../manifests/fake-teams-api.yaml) deployment is provided +in the manifests folder to set up a basic API around whatever services is used +for user management. The Teams API's URL is set in the operator's +[configuration](reference/operator_parameters.md#automatic-creation-of-human-users-in-the-database) +and `enable_teams_api` must be set to `true`. There are more settings available +to choose superusers, group roles, [PAM configuration](https://github.com/CyberDem0n/pam-oauth2) +etc. An OAuth2 token can be passed to the Teams API via a secret. The name for +this secret is configurable with the `oauth_token_secret_name` parameter. + +## Resource definition + +The compute resources to be used for the Postgres containers in the pods can be +specified in the postgresql cluster manifest. + +```yaml +spec: + resources: + requests: + cpu: 10m + memory: 100Mi + limits: + cpu: 300m + memory: 300Mi +``` + +The minimum limit to properly run the `postgresql` resource is `256m` for `cpu` +and `256Mi` for `memory`. If a lower value is set in the manifest the operator +will cancel ADD or UPDATE events on this resource with an error. If no +resources are defined in the manifest the operator will obtain the configured +[default requests](reference/operator_parameters.md#kubernetes-resource-requests). + ## Use taints and tolerations for dedicated PostgreSQL nodes To ensure Postgres pods are running on nodes without any other application pods, @@ -189,12 +245,7 @@ you can use [taints and tolerations](https://kubernetes.io/docs/concepts/configu and configure the required toleration in the manifest. ```yaml -apiVersion: "acid.zalan.do/v1" -kind: postgresql -metadata: - name: acid-minimal-cluster spec: - teamId: "ACID" tolerations: - key: postgres operator: Exists @@ -212,11 +263,6 @@ section in the spec. There are two options here: ### Clone directly ```yaml -apiVersion: "acid.zalan.do/v1" -kind: postgresql - -metadata: - name: acid-test-cluster spec: clone: cluster: "acid-batman" @@ -232,11 +278,6 @@ means that you can clone only from clusters within the same namespace. ### Clone from S3 ```yaml -apiVersion: "acid.zalan.do/v1" -kind: postgresql - -metadata: - name: acid-test-cluster spec: clone: uid: "efd12e58-5786-11e8-b5a7-06148230260c" @@ -265,10 +306,6 @@ For non AWS S3 following settings can be set to support cloning from other S3 implementations: ```yaml -apiVersion: "acid.zalan.do/v1" -kind: postgresql -metadata: - name: acid-test-cluster spec: clone: uid: "efd12e58-5786-11e8-b5a7-06148230260c" @@ -305,7 +342,7 @@ Things to note: - There is no way to transform a non-standby cluster to a standby cluster through the operator. Adding the standby section to the manifest of a running Postgres cluster will have no effect. However, it can be done through Patroni - by adding the [standby_cluster] (https://github.com/zalando/patroni/blob/bd2c54581abb42a7d3a3da551edf0b8732eefd27/docs/replica_bootstrap.rst#standby-cluster) + by adding the [standby_cluster](https://github.com/zalando/patroni/blob/bd2c54581abb42a7d3a3da551edf0b8732eefd27/docs/replica_bootstrap.rst#standby-cluster) section using `patronictl edit-config`. Note that the transformed standby cluster will not be doing any streaming. It will be in standby mode and allow read-only transactions only. @@ -317,13 +354,7 @@ used for log aggregation, monitoring, backups or other tasks. A sidecar can be specified like this: ```yaml -apiVersion: "acid.zalan.do/v1" -kind: postgresql - -metadata: - name: acid-minimal-cluster spec: - ... sidecars: - name: "container-name" image: "company/image:tag" @@ -350,6 +381,10 @@ variables are always passed to sidecars: The PostgreSQL volume is shared with sidecars and is mounted at `/home/postgres/pgdata`. +**Note**: The operator will not create a cluster if sidecar containers are +specified but globally disabled in the configuration. The `enable_sidecars` +option must be set to `true`. + ## InitContainers Support Each cluster can specify arbitrary init containers to run. These containers can @@ -357,13 +392,7 @@ be used to run custom actions before any normal and sidecar containers start. An init container can be specified like this: ```yaml -apiVersion: "acid.zalan.do/v1" -kind: postgresql - -metadata: - name: acid-minimal-cluster spec: - ... initContainers: - name: "container-name" image: "company/image:tag" @@ -374,18 +403,17 @@ spec: `initContainers` accepts full `v1.Container` definition. +**Note**: The operator will not create a cluster if `initContainers` are +specified but globally disabled in the configuration. The +`enable_init_containers` option must be set to `true`. + ## Increase volume size -PostgreSQL operator supports statefulset volume resize if you're using the +Postgres operator supports statefulset volume resize if you're using the operator on top of AWS. For that you need to change the size field of the volume description in the cluster manifest and apply the change: -``` -apiVersion: "acid.zalan.do/v1" -kind: postgresql - -metadata: - name: acid-test-cluster +```yaml spec: volume: size: 5Gi # new volume size @@ -414,7 +442,8 @@ size of volumes that correspond to the previously running pods is not changed. You can enable logical backups from the cluster manifest by adding the following parameter in the spec section: -``` +```yaml +spec: enableLogicalBackup: true ``` diff --git a/e2e/Makefile b/e2e/Makefile index 0a4f42bae..77059f3eb 100644 --- a/e2e/Makefile +++ b/e2e/Makefile @@ -23,13 +23,12 @@ ifndef GOPATH GOPATH := $(HOME)/go endif -KIND_PATH := $(GOPATH)/bin PATH := $(GOPATH)/bin:$(PATH) default: tools clean: - rm -fr manifests + rm -rf manifests copy: clean mkdir manifests @@ -43,10 +42,7 @@ push: docker tools: docker # install pinned version of 'kind' - # leave the name as is to avoid overwriting official binary named `kind` - wget https://github.com/kubernetes-sigs/kind/releases/download/v0.4.0/kind-linux-amd64 - chmod +x kind-linux-amd64 - mv kind-linux-amd64 $(KIND_PATH) + GO111MODULE=on go get sigs.k8s.io/kind@v0.5.1 test: ./run.sh diff --git a/e2e/run.sh b/e2e/run.sh index 237960b89..c7825bfd3 100755 --- a/e2e/run.sh +++ b/e2e/run.sh @@ -30,15 +30,15 @@ function pull_images(){ function start_kind(){ # avoid interference with previous test runs - if [[ $(kind-linux-amd64 get clusters | grep "^${cluster_name}*") != "" ]] + if [[ $(kind get clusters | grep "^${cluster_name}*") != "" ]] then - kind-linux-amd64 delete cluster --name ${cluster_name} + kind delete cluster --name ${cluster_name} fi - kind-linux-amd64 create cluster --name ${cluster_name} --config kind-cluster-postgres-operator-e2e-tests.yaml - kind-linux-amd64 load docker-image "${operator_image}" --name ${cluster_name} - kind-linux-amd64 load docker-image "${e2e_test_image}" --name ${cluster_name} - KUBECONFIG="$(kind-linux-amd64 get kubeconfig-path --name=${cluster_name})" + kind create cluster --name ${cluster_name} --config kind-cluster-postgres-operator-e2e-tests.yaml + kind load docker-image "${operator_image}" --name ${cluster_name} + kind load docker-image "${e2e_test_image}" --name ${cluster_name} + KUBECONFIG="$(kind get kubeconfig-path --name=${cluster_name})" export KUBECONFIG } @@ -58,7 +58,7 @@ function run_tests(){ function clean_up(){ unset KUBECONFIG - kind-linux-amd64 delete cluster --name ${cluster_name} + kind delete cluster --name ${cluster_name} rm -rf ${kubeconfig_path} } diff --git a/e2e/tests/test_e2e.py b/e2e/tests/test_e2e.py index 52aa0549a..88a7f1f34 100644 --- a/e2e/tests/test_e2e.py +++ b/e2e/tests/test_e2e.py @@ -182,17 +182,12 @@ class EndToEndTestCase(unittest.TestCase): # update the cluster-wide image of the logical backup pod image = "test-image-name" - config_map_patch = { + patch_logical_backup_image = { "data": { "logical_backup_docker_image": image, } } - k8s.api.core_v1.patch_namespaced_config_map("postgres-operator", "default", config_map_patch) - - operator_pod = k8s.api.core_v1.list_namespaced_pod( - 'default', label_selector="name=postgres-operator").items[0].metadata.name - k8s.api.core_v1.delete_namespaced_pod(operator_pod, "default") # restart reloads the conf - k8s.wait_for_operator_pod_start() + k8s.update_config(patch_logical_backup_image) jobs = k8s.get_logical_backup_job().items actual_image = jobs[0].spec.job_template.spec.template.spec.containers[0].image @@ -319,6 +314,14 @@ class K8s: def wait_for_logical_backup_job_creation(self): self.wait_for_logical_backup_job(expected_num_of_jobs=1) + def update_config(self, config_map_patch): + self.api.core_v1.patch_namespaced_config_map("postgres-operator", "default", config_map_patch) + + operator_pod = self.api.core_v1.list_namespaced_pod( + 'default', label_selector="name=postgres-operator").items[0].metadata.name + self.api.core_v1.delete_namespaced_pod(operator_pod, "default") # restart reloads the conf + self.wait_for_operator_pod_start() + def create_with_kubectl(self, path): subprocess.run(["kubectl", "create", "-f", path]) diff --git a/glide.lock b/glide.lock deleted file mode 100644 index 8b8f2814d..000000000 --- a/glide.lock +++ /dev/null @@ -1,308 +0,0 @@ -hash: e4d0d48b4142d5d335e48e030621564207f3aa288ea1e41e0d1e4d29135de3e8 -updated: 2019-02-25T14:54:51.736946406+01:00 -imports: -- name: github.com/aws/aws-sdk-go - version: e8b22c9937cae1fee9bc364a88f3752cc4d1ac2f - subpackages: - - aws - - aws/awserr - - aws/awsutil - - aws/client - - aws/client/metadata - - aws/corehandlers - - aws/credentials - - aws/credentials/ec2rolecreds - - aws/credentials/endpointcreds - - aws/credentials/processcreds - - aws/credentials/stscreds - - aws/csm - - aws/defaults - - aws/ec2metadata - - aws/endpoints - - aws/request - - aws/session - - aws/signer/v4 - - internal/ini - - internal/sdkio - - internal/sdkrand - - internal/sdkuri - - internal/shareddefaults - - private/protocol - - private/protocol/ec2query - - private/protocol/query - - private/protocol/query/queryutil - - private/protocol/rest - - private/protocol/xml/xmlutil - - service/ec2 - - service/sts -- name: github.com/davecgh/go-spew - version: 782f4967f2dc4564575ca782fe2d04090b5faca8 - subpackages: - - spew -- name: github.com/docker/spdystream - version: 449fdfce4d962303d702fec724ef0ad181c92528 - subpackages: - - spdy -- name: github.com/ghodss/yaml - version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee -- name: github.com/gogo/protobuf - version: c0656edd0d9eab7c66d1eb0c568f9039345796f7 - subpackages: - - proto - - sortkeys -- name: github.com/golang/glog - version: 44145f04b68cf362d9c4df2182967c2275eaefed -- name: github.com/golang/protobuf - version: b4deda0973fb4c70b50d226b1af49f3da59f5265 - subpackages: - - proto - - ptypes - - ptypes/any - - ptypes/duration - - ptypes/timestamp -- name: github.com/google/btree - version: 7d79101e329e5a3adf994758c578dab82b90c017 -- name: github.com/google/gofuzz - version: 44d81051d367757e1c7c6a5a86423ece9afcf63c -- name: github.com/googleapis/gnostic - version: 0c5108395e2debce0d731cf0287ddf7242066aba - subpackages: - - OpenAPIv2 - - compiler - - extensions -- name: github.com/gregjones/httpcache - version: 787624de3eb7bd915c329cba748687a3b22666a6 - subpackages: - - diskcache -- name: github.com/hashicorp/golang-lru - version: a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4 - subpackages: - - simplelru -- name: github.com/imdario/mergo - version: 6633656539c1639d9d78127b7d47c622b5d7b6dc -- name: github.com/jmespath/go-jmespath - version: c2b33e8439af944379acbdd9c3a5fe0bc44bd8a5 -- name: github.com/json-iterator/go - version: f2b4162afba35581b6d4a50d3b8f34e33c144682 -- name: github.com/konsorten/go-windows-terminal-sequences - version: 5c8c8bd35d3832f5d134ae1e1e375b69a4d25242 -- name: github.com/kr/text - version: e2ffdb16a802fe2bb95e2e35ff34f0e53aeef34f -- name: github.com/lib/pq - version: 90697d60dd844d5ef6ff15135d0203f65d2f53b8 - subpackages: - - oid -- name: github.com/modern-go/concurrent - version: bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94 -- name: github.com/modern-go/reflect2 - version: 05fbef0ca5da472bbf96c9322b84a53edc03c9fd -- name: github.com/mohae/deepcopy - version: c48cc78d482608239f6c4c92a4abd87eb8761c90 -- name: github.com/motomux/pretty - version: b2aad2c9a95d14eb978f29baa6e3a5c3c20eef30 -- name: github.com/peterbourgon/diskv - version: 5f041e8faa004a95c88a202771f4cc3e991971e6 -- name: github.com/sirupsen/logrus - version: e1e72e9de974bd926e5c56f83753fba2df402ce5 -- name: github.com/spf13/pflag - version: 583c0c0531f06d5278b7d917446061adc344b5cd -- name: golang.org/x/crypto - version: 49796115aa4b964c318aad4f3084fdb41e9aa067 - subpackages: - - bcrypt - - blowfish - - ssh/terminal -- name: golang.org/x/net - version: 1c05540f6879653db88113bc4a2b70aec4bd491f - subpackages: - - context - - http2 - - http2/hpack - - idna - - lex/httplex -- name: golang.org/x/sys - version: 95c6576299259db960f6c5b9b69ea52422860fce - subpackages: - - unix - - windows -- name: golang.org/x/text - version: b19bf474d317b857955b12035d2c5acb57ce8b01 - subpackages: - - secure/bidirule - - transform - - unicode/bidi - - unicode/norm -- name: golang.org/x/time - version: f51c12702a4d776e4c1fa9b0fabab841babae631 - subpackages: - - rate -- name: gopkg.in/inf.v0 - version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 -- name: gopkg.in/yaml.v2 - version: 5420a8b6744d3b0345ab293f6fcba19c978f1183 -- name: k8s.io/api - version: 2d6f90ab1293a1fb871cf149423ebb72aa7423aa - subpackages: - - admission/v1beta1 - - admissionregistration/v1alpha1 - - admissionregistration/v1beta1 - - apps/v1 - - apps/v1beta1 - - apps/v1beta2 - - authentication/v1 - - authentication/v1beta1 - - authorization/v1 - - authorization/v1beta1 - - autoscaling/v1 - - autoscaling/v2beta1 - - batch/v1 - - batch/v1beta1 - - batch/v2alpha1 - - certificates/v1beta1 - - core/v1 - - events/v1beta1 - - extensions/v1beta1 - - networking/v1 - - policy/v1beta1 - - rbac/v1 - - rbac/v1alpha1 - - rbac/v1beta1 - - scheduling/v1alpha1 - - scheduling/v1beta1 - - settings/v1alpha1 - - storage/v1 - - storage/v1alpha1 - - storage/v1beta1 -- name: k8s.io/apiextensions-apiserver - version: cc9cd5d998df84cc405d398e9030d29c95acff18 - subpackages: - - pkg/apis/apiextensions - - pkg/apis/apiextensions/v1beta1 - - pkg/client/clientset/clientset - - pkg/client/clientset/clientset/scheme - - pkg/client/clientset/clientset/typed/apiextensions/v1beta1 -- name: k8s.io/apimachinery - version: 103fd098999dc9c0c88536f5c9ad2e5da39373ae - subpackages: - - pkg/api/errors - - pkg/api/meta - - pkg/api/resource - - pkg/apis/meta/internalversion - - pkg/apis/meta/v1 - - pkg/apis/meta/v1/unstructured - - pkg/apis/meta/v1beta1 - - pkg/conversion - - pkg/conversion/queryparams - - pkg/fields - - pkg/labels - - pkg/runtime - - pkg/runtime/schema - - pkg/runtime/serializer - - pkg/runtime/serializer/json - - pkg/runtime/serializer/protobuf - - pkg/runtime/serializer/recognizer - - pkg/runtime/serializer/streaming - - pkg/runtime/serializer/versioning - - pkg/selection - - pkg/types - - pkg/util/cache - - pkg/util/clock - - pkg/util/diff - - pkg/util/errors - - pkg/util/framer - - pkg/util/httpstream - - pkg/util/httpstream/spdy - - pkg/util/intstr - - pkg/util/json - - pkg/util/mergepatch - - pkg/util/net - - pkg/util/remotecommand - - pkg/util/runtime - - pkg/util/sets - - pkg/util/strategicpatch - - pkg/util/validation - - pkg/util/validation/field - - pkg/util/wait - - pkg/util/yaml - - pkg/version - - pkg/watch - - third_party/forked/golang/json - - third_party/forked/golang/netutil - - third_party/forked/golang/reflect -- name: k8s.io/client-go - version: 1f13a808da65775f22cbf47862c4e5898d8f4ca1 - subpackages: - - discovery - - discovery/fake - - kubernetes - - kubernetes/scheme - - kubernetes/typed/admissionregistration/v1alpha1 - - kubernetes/typed/admissionregistration/v1beta1 - - kubernetes/typed/apps/v1 - - kubernetes/typed/apps/v1beta1 - - kubernetes/typed/apps/v1beta2 - - kubernetes/typed/authentication/v1 - - kubernetes/typed/authentication/v1beta1 - - kubernetes/typed/authorization/v1 - - kubernetes/typed/authorization/v1beta1 - - kubernetes/typed/autoscaling/v1 - - kubernetes/typed/autoscaling/v2beta1 - - kubernetes/typed/batch/v1 - - kubernetes/typed/batch/v1beta1 - - kubernetes/typed/batch/v2alpha1 - - kubernetes/typed/certificates/v1beta1 - - kubernetes/typed/core/v1 - - kubernetes/typed/events/v1beta1 - - kubernetes/typed/extensions/v1beta1 - - kubernetes/typed/networking/v1 - - kubernetes/typed/policy/v1beta1 - - kubernetes/typed/rbac/v1 - - kubernetes/typed/rbac/v1alpha1 - - kubernetes/typed/rbac/v1beta1 - - kubernetes/typed/scheduling/v1alpha1 - - kubernetes/typed/scheduling/v1beta1 - - kubernetes/typed/settings/v1alpha1 - - kubernetes/typed/storage/v1 - - kubernetes/typed/storage/v1alpha1 - - kubernetes/typed/storage/v1beta1 - - pkg/apis/clientauthentication - - pkg/apis/clientauthentication/v1alpha1 - - pkg/apis/clientauthentication/v1beta1 - - pkg/version - - plugin/pkg/client/auth/exec - - rest - - rest/watch - - testing - - tools/auth - - tools/cache - - tools/clientcmd - - tools/clientcmd/api - - tools/clientcmd/api/latest - - tools/clientcmd/api/v1 - - tools/metrics - - tools/pager - - tools/reference - - tools/remotecommand - - transport - - transport/spdy - - util/buffer - - util/cert - - util/connrotation - - util/exec - - util/flowcontrol - - util/homedir - - util/integer - - util/retry -- name: k8s.io/code-generator - version: 6702109cc68eb6fe6350b83e14407c8d7309fd1a -- name: k8s.io/gengo - version: 906d99f89cd644eecf75ab547b29bf9f876f0b59 -- name: k8s.io/kube-openapi - version: 91cfa479c814065e420cee7ed227db0f63a5854e - subpackages: - - pkg/builder - - pkg/common - - pkg/handler - - pkg/util - - pkg/util/proto -testImports: [] diff --git a/glide.yaml b/glide.yaml deleted file mode 100644 index 986970887..000000000 --- a/glide.yaml +++ /dev/null @@ -1,23 +0,0 @@ -package: github.com/zalando/postgres-operator -import: -- package: github.com/sirupsen/logrus - version: ^1.0.1 -- package: github.com/aws/aws-sdk-go - version: ^1.8.24 - subpackages: - - aws - - aws/session - - service/ec2 -- package: github.com/lib/pq -- package: github.com/motomux/pretty -- package: k8s.io/apimachinery - version: kubernetes-1.11.3-beta.0 -- package: k8s.io/apiextensions-apiserver - version: kubernetes-1.11.3-beta.0 -- package: k8s.io/client-go - version: kubernetes-1.11.3-beta.0 -- package: k8s.io/code-generator - version: kubernetes-1.11.3-beta.0 -- package: k8s.io/gengo -- package: gopkg.in/yaml.v2 -- package: github.com/mohae/deepcopy diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..36686dcf6 --- /dev/null +++ b/go.mod @@ -0,0 +1,25 @@ +module github.com/zalando/postgres-operator + +go 1.12 + +require ( + github.com/aws/aws-sdk-go v1.25.44 + github.com/emicklei/go-restful v2.9.6+incompatible // indirect + github.com/evanphx/json-patch v4.5.0+incompatible // indirect + github.com/googleapis/gnostic v0.3.0 // indirect + github.com/imdario/mergo v0.3.8 // indirect + github.com/lib/pq v1.2.0 + github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d + github.com/sirupsen/logrus v1.4.2 + golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 // indirect + golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect + golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 // indirect + golang.org/x/tools v0.0.0-20191209225234-22774f7dae43 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gopkg.in/yaml.v2 v2.2.4 + k8s.io/api v0.0.0-20191121015604-11707872ac1c + k8s.io/apiextensions-apiserver v0.0.0-20191204090421-cd61debedab5 + k8s.io/apimachinery v0.0.0-20191203211716-adc6f4cd9e7d + k8s.io/client-go v0.0.0-20191204082520-bc9b51d240b2 + k8s.io/code-generator v0.0.0-20191121015212-c4c8f8345c7e +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..f85dd060f --- /dev/null +++ b/go.sum @@ -0,0 +1,466 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.25.44 h1:n9ahFoiyn66smjF34hYr3tb6/ZdBcLuFz7BCDhHyJ7I= +github.com/aws/aws-sdk-go v1.25.44/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.6+incompatible h1:tfrHha8zJ01ywiOEC1miGY8st1/igzWB8OmvPgoYX7w= +github.com/emicklei/go-restful v2.9.6+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= +github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/spec v0.19.3 h1:0XRyw8kguri6Yw4SxhsQA/atC88yqrk0+G4YhI2wabc= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.3.0 h1:CcQijm0XKekKjP/YCz28LXVSpgguuB+nCxaSjCe09y0= +github.com/googleapis/gnostic v0.3.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d h1:LznySqW8MqVeFh+pW6rOkFdld9QQ7jRydBKKM6jyPVI= +github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d/go.mod h1:u3hJ0kqCQu/cPpsu3RbCOPZ0d7V3IjPjv1adNRleM9I= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495 h1:I6A9Ag9FpEKOjcKrRNjQkPHawoXIhKyTGfvvjFAiiAk= +golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191209225234-22774f7dae43 h1:NfPq5mgc5ArFgVLCpeS4z07IoxSAqVfV/gQ5vxdgaxI= +golang.org/x/tools v0.0.0-20191209225234-22774f7dae43/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485 h1:OB/uP/Puiu5vS5QMRPrXCDWUPb+kt8f1KW8oQzFejQw= +gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e h1:jRyg0XfpwWlhEV8mDfdNGBeSJM2fuyh9Yjrnd8kF2Ts= +gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.0.0-20191121015604-11707872ac1c h1:Z87my3sF4WhG0OMxzARkWY/IKBtOr+MhXZAb4ts6qFc= +k8s.io/api v0.0.0-20191121015604-11707872ac1c/go.mod h1:R/s4gKT0V/cWEnbQa9taNRJNbWUK57/Dx6cPj6MD3A0= +k8s.io/apiextensions-apiserver v0.0.0-20191204090421-cd61debedab5 h1:g+GvnbGqLU1Jxb/9iFm/BFcmkqG9HdsGh52+wHirpsM= +k8s.io/apiextensions-apiserver v0.0.0-20191204090421-cd61debedab5/go.mod h1:CPw0IHz1YrWGy0+8mG/76oTHXvChlgCb3EAezKQKB2I= +k8s.io/apimachinery v0.0.0-20191121015412-41065c7a8c2a/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apimachinery v0.0.0-20191123233150-4c4803ed55e3/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apimachinery v0.0.0-20191128180518-03184f823e28/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apimachinery v0.0.0-20191203211716-adc6f4cd9e7d h1:q+OZmYewHJeMCzwpHkXlNTtk5bvaUMPCikKvf77RBlo= +k8s.io/apimachinery v0.0.0-20191203211716-adc6f4cd9e7d/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apiserver v0.0.0-20191204084332-137a9d3b886b/go.mod h1:itgfam5HJbT/4b2BGfpUkkxfheMmDH+Ix+tEAP3uqZk= +k8s.io/client-go v0.0.0-20191204082517-8c19b9f4a642/go.mod h1:HMVIZ0dPop3WCrPEaJ+v5/94cjt56avdDFshpX0Fjvo= +k8s.io/client-go v0.0.0-20191204082519-e9644b2e3edc/go.mod h1:5lSG1yeDZVwDYAHe9VK48SCe5zmcnkAcf2Mx59TuhmM= +k8s.io/client-go v0.0.0-20191204082520-bc9b51d240b2 h1:T2HGghBOPAOEjWuIyFSeCsWEwsxa6unkBvy3PHfqonM= +k8s.io/client-go v0.0.0-20191204082520-bc9b51d240b2/go.mod h1:5lSG1yeDZVwDYAHe9VK48SCe5zmcnkAcf2Mx59TuhmM= +k8s.io/code-generator v0.0.0-20191121015212-c4c8f8345c7e h1:HB9Zu5ZUvJfNpLiTPhz+CebVKV8C39qTBMQkAgAZLNw= +k8s.io/code-generator v0.0.0-20191121015212-c4c8f8345c7e/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= +k8s.io/component-base v0.0.0-20191204083903-0d4d24e738e4/go.mod h1:8VIh1jErItC4bg9hLBkPneyS77Tin8KwSzbYepHJnQI= +k8s.io/component-base v0.0.0-20191204083906-3ac1376c73aa/go.mod h1:mECWvHCPhJudDVDMtBl+AIf/YnTMp5r1F947OYFUwP0= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20190822140433-26a664648505 h1:ZY6yclUKVbZ+SdWnkfY+Je5vrMpKOxmGeKRbsXVmqYM= +k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/hack/tools.go b/hack/tools.go new file mode 100644 index 000000000..18432c1b1 --- /dev/null +++ b/hack/tools.go @@ -0,0 +1,19 @@ +// +build tools + +/* +Copyright 2019 The Kubernetes Authors. +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. +*/ + +// This package imports things required by build scripts, to force `go mod` to see them as dependencies +package tools + +import _ "k8s.io/code-generator" diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index 70f041d07..280da9385 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -o errexit set -o nounset @@ -7,7 +7,7 @@ set -o pipefail SCRIPT_ROOT=$(dirname ${BASH_SOURCE})/.. CODEGEN_PKG=${CODEGEN_PKG:-$(cd ${SCRIPT_ROOT}; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ${GOPATH}/src/k8s.io/code-generator)} -vendor/k8s.io/code-generator/generate-groups.sh all \ +bash "${CODEGEN_PKG}/generate-groups.sh" all \ github.com/zalando/postgres-operator/pkg/generated github.com/zalando/postgres-operator/pkg/apis \ - acid.zalan.do:v1 \ - --go-header-file ${SCRIPT_ROOT}/hack/custom-boilerplate.go.txt + "acid.zalan.do:v1" \ + --go-header-file "${SCRIPT_ROOT}"/hack/custom-boilerplate.go.txt diff --git a/hack/verify-codegen.sh b/hack/verify-codegen.sh index 904586d05..68710015e 100755 --- a/hack/verify-codegen.sh +++ b/hack/verify-codegen.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -o errexit set -o nounset diff --git a/kubectl-pg/README.md b/kubectl-pg/README.md new file mode 100644 index 000000000..c6d4c38ee --- /dev/null +++ b/kubectl-pg/README.md @@ -0,0 +1,125 @@ +# Kubectl Plugin for Zalando's Postgres Operator + +## Google Summer of Code 2019 + +This plugin is a prototype developed as a part of GSoC 2019 under the organisation +**The Postgres Operator** + +### GSoC Proposal + +[kubectl pg proposal](https://docs.google.com/document/d/1-WMy9HkfZ1XnnMbzplMe9rCzKrRMGaMz4owLVXXPb7w/edit) + +### Weekly Reports + +https://github.com/VineethReddy02/GSoC-Kubectl-Plugin-for-Postgres-Operator-tracker + + ### Final Project Report + + https://gist.github.com/VineethReddy02/159283bd368a710379eaf0f6bd60a40a + + +### Installtion of kubectl pg plugin + +This project uses Go Modules for dependency management to build locally +Install go and enable go modules ```export GO111MODULE=on``` +From Go >=1.13 Go modules will be enabled by default +``` +# Assumes you have a working KUBECONFIG +$ GO111MODULE="on" +# As of now go by default doesn't support Go mods. So explicit enabling is required. +$ GOPATH/src/github.com/zalando/postgres-operator/kubectl-pg go mod vendor +# This generate a vendor directory with all dependencies needed by the plugin. +$ $GOPATH/src/github.com/zalando/postgres-operator/kubectl-pg go install +# This will place the kubectl-pg binary in your $GOPATH/bin +``` + +### Before using the kubectl pg plugin make sure to set KUBECONFIG env varibale + +Ideally KUBECONFIG is found in $HOME/.kube/config else specify the KUBECONFIG path here. + +```export KUBECONFIG=$HOME/.kube/config``` + +### To list all commands available in kubectl pg + +```kubectl pg --help``` (or) ```kubectl pg``` + +### This basically means the operator pod managed to start, so our operator is installed. + +```kubectl pg check``` + +### To create postgresql cluster using manifest file + +```kubectl pg create -f acid-minimal-cluster.yaml``` + +### To update existing cluster using manifest file + +```kubectl pg update -f acid-minimal-cluster.yaml``` + +### To delete existing cluster using manifest file + +```kubectl pg delete -f acid-minimal-cluster.yaml``` + +### To delete existing cluster using cluster name + +```kubectl pg delete acid-minimal-cluster``` + +--namespace or -n flag to specify namespace if cluster is in other namespace. + +```kubectl pg delete acid-minimal-cluster -n namespace01``` + +### To list postgres resources for the current namespace + +```kubectl pg list``` + +### To list postgres resources across namespaces + +```kubectl pg list all``` + +### To add-user and it's roles to an existing pg cluster + +```kubectl pg add-user USER01 -p CREATEDB,LOGIN -c acid-minimal-cluster``` + +Privileges can only be [SUPERUSER, REPLICATION, INHERIT, LOGIN, NOLOGIN, CREATEROLE, CREATEDB, BYPASSURL] + +Note: A login user is created by default unless NOLOGIN is specified, in which case the operator creates a role. + +### To add-db and it's owner to an existing pg cluster + +```kubectl pg add-db DB01 -o OWNER01 -c acid-minimal-cluster``` + +### To extend volume for an existing pg cluster + +```kubectl pg ext-volume 2Gi -c acid-minimal-cluster``` + +### To find the version of postgres operator and kubectl plugin + +```kubectl pg version (optional -n NAMESPACE allows to know specific to a namespace)``` + +### To connect to the shell of a postgres pod + +```kubectl pg connect -c CLUSTER``` #This connects to a random pod +```kubectl pg connect -c CLUSTER -m``` #This connects the master +```kubectl pg connect -c CLUSTER -r 0``` #This connects to the desired replica + +### To connect to the psql prompt + +```kubectl pg connect -c CLUSTER -p -u username``` #This connects to a random pod. Not master +```kubectl pg connect -c CLUSTER -m -p -u username``` #This connects the master +```kubectl pg connect -c CLUSTER -r 0 -p -u username``` #This connects to the desired replica + +Note: -p represents psql prompt + +### To get the logs of postgres operator + +```kubectl pg logs -o``` + +### To get the logs of the postgres cluster + +```kubectl pg logs -c CLUSTER``` #Fetches the logs of a random pod. Not master +```kubectl pg logs -c CLUSTER -m``` #Fetches the logs of master +```kubectl pg logs -c CLUSTER -r 2``` #Fecthes the logs of specified replica + +## Development + +- When making changes to plugin make sure to change the major or patch version +of plugin in ```build.sh``` and run ```./build.sh``` diff --git a/kubectl-pg/build.sh b/kubectl-pg/build.sh new file mode 100755 index 000000000..a81bf54fc --- /dev/null +++ b/kubectl-pg/build.sh @@ -0,0 +1,4 @@ + +VERSION=1.0 +sed -i "s/KubectlPgVersion string = \"[^\"]*\"/KubectlPgVersion string = \"${VERSION}\"/" cmd/version.go +go install \ No newline at end of file diff --git a/kubectl-pg/cmd/addDb.go b/kubectl-pg/cmd/addDb.go new file mode 100644 index 000000000..1da426703 --- /dev/null +++ b/kubectl-pg/cmd/addDb.go @@ -0,0 +1,112 @@ +/* +Copyright © 2019 Vineeth Pothulapati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "encoding/json" + "fmt" + "github.com/spf13/cobra" + PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "log" +) + +// addDbCmd represents the addDb command +var addDbCmd = &cobra.Command{ + Use: "add-db", + Short: "Adds a DB and its owner to a Postgres cluster. The owner role is created if it does not exist", + Long: `Adds a new DB to the Postgres cluster. Owner needs to be specified by the -o flag, cluster with -c flag.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) > 0 { + dbName := args[0] + dbOwner, _ := cmd.Flags().GetString("owner") + clusterName, _ := cmd.Flags().GetString("cluster") + addDb(dbName, dbOwner, clusterName) + } else { + fmt.Println("database name can't be empty. Use kubectl pg add-db [-h | --help] for more info") + } + + }, + Example: ` +kubectl pg add-db db01 -o owner01 -c cluster01 +`, +} + +// add db and it's owner to the cluster +func addDb(dbName string, dbOwner string, clusterName string) { + config := getConfig() + postgresConfig, err := PostgresqlLister.NewForConfig(config) + if err != nil { + log.Fatal(err) + } + + namespace := getCurrentNamespace() + postgresql, err := postgresConfig.Postgresqls(namespace).Get(clusterName, metav1.GetOptions{}) + if err != nil { + log.Fatal(err) + } + + var dbOwnerExists bool + dbUsers := postgresql.Spec.Users + for key, _ := range dbUsers { + if key == dbOwner { + dbOwnerExists = true + } + } + var patch []byte + // validating reserved DB names + if dbOwnerExists && dbName != "postgres" && dbName != "template0" && dbName != "template1" { + patch = dbPatch(dbName, dbOwner) + } else if !dbOwnerExists { + log.Fatal("The provided db-owner doesn't exist") + } else { + log.Fatal("The provided db-name is reserved by postgres") + } + + updatedPostgres, err := postgresConfig.Postgresqls(namespace).Patch(postgresql.Name, types.MergePatchType, patch, "") + if err != nil { + log.Fatal(err) + } + + if updatedPostgres.ResourceVersion != postgresql.ResourceVersion { + fmt.Printf("Created new database %s with owner %s in PostgreSQL cluster %s.\n", dbName, dbOwner, updatedPostgres.Name) + } else { + fmt.Printf("postgresql %s is unchanged.\n", updatedPostgres.Name) + } +} + +func dbPatch(dbname string, owner string) []byte { + ins := map[string]map[string]map[string]string{"spec": {"databases": {dbname: owner}}} + patchInstances, err := json.Marshal(ins) + if err != nil { + log.Fatal(err, "unable to parse patch for add-db") + } + return patchInstances +} + +func init() { + addDbCmd.Flags().StringP("owner", "o", "", "provide owner of the database.") + addDbCmd.Flags().StringP("cluster", "c", "", "provide a postgres cluster name.") + rootCmd.AddCommand(addDbCmd) +} diff --git a/kubectl-pg/cmd/addUser.go b/kubectl-pg/cmd/addUser.go new file mode 100644 index 000000000..df38c44b8 --- /dev/null +++ b/kubectl-pg/cmd/addUser.go @@ -0,0 +1,142 @@ +/* +Copyright © 2019 Vineeth Pothulapati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "encoding/json" + "fmt" + "github.com/spf13/cobra" + PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "log" + "strings" +) + +var allowedPrivileges = []string{"SUPERUSER", "REPLICATION", "INHERIT", "LOGIN", "NOLOGIN", "CREATEROLE", "CREATEDB", "BYPASSURL"} + +// addUserCmd represents the addUser command +var addUserCmd = &cobra.Command{ + Use: "add-user", + Short: "Adds a user to the postgres cluster with given privileges", + Long: `Adds a user to the postgres cluster. You can add privileges as well with -p flag.`, + Run: func(cmd *cobra.Command, args []string) { + clusterName, _ := cmd.Flags().GetString("cluster") + privileges, _ := cmd.Flags().GetString("privileges") + + if len(args) > 0 { + user := args[0] + var permissions []string + var perms []string + + if privileges != "" { + parsedRoles := strings.Replace(privileges, ",", " ", -1) + parsedRoles = strings.ToUpper(parsedRoles) + permissions = strings.Fields(parsedRoles) + var invalidPerms []string + + for _, userPrivilege := range permissions { + validPerm := false + for _, privilege := range allowedPrivileges { + if privilege == userPrivilege { + perms = append(perms, userPrivilege) + validPerm = true + } + } + if !validPerm { + invalidPerms = append(invalidPerms, userPrivilege) + } + } + + if len(invalidPerms) > 0 { + fmt.Printf("Invalid privileges %s\n", invalidPerms) + return + } + } + addUser(user, clusterName, perms) + } + }, + Example: ` +kubectl pg add-user user01 -p login,createdb -c cluster01 +`, +} + +// add user to the cluster with provided permissions +func addUser(user string, clusterName string, permissions []string) { + config := getConfig() + postgresConfig, err := PostgresqlLister.NewForConfig(config) + if err != nil { + log.Fatal(err) + } + + namespace := getCurrentNamespace() + postgresql, err := postgresConfig.Postgresqls(namespace).Get(clusterName, metav1.GetOptions{}) + if err != nil { + log.Fatal(err) + } + + setUsers := make(map[string]bool) + for _, k := range permissions { + setUsers[k] = true + } + + if existingRoles, key := postgresql.Spec.Users[user]; key { + for _, k := range existingRoles { + setUsers[k] = true + } + } + + Privileges := []string{} + for keys, values := range setUsers { + if values { + Privileges = append(Privileges, keys) + } + } + + patch := applyUserPatch(user, Privileges) + updatedPostgresql, err := postgresConfig.Postgresqls(namespace).Patch(postgresql.Name, types.MergePatchType, patch, "") + if err != nil { + log.Fatal(err) + } + + if updatedPostgresql.ResourceVersion != postgresql.ResourceVersion { + fmt.Printf("postgresql %s is updated with new user %s and with privileges %s.\n", updatedPostgresql.Name, user, permissions) + } else { + fmt.Printf("postgresql %s is unchanged.\n", updatedPostgresql.Name) + } +} + +func applyUserPatch(user string, value []string) []byte { + ins := map[string]map[string]map[string][]string{"spec": {"users": {user: value}}} + patchInstances, err := json.Marshal(ins) + if err != nil { + log.Fatal(err, "unable to parse number of instances json") + } + return patchInstances +} + +func init() { + addUserCmd.Flags().StringP("cluster", "c", "", "add user to the provided cluster.") + addUserCmd.Flags().StringP("privileges", "p", "", "add privileges separated by commas without spaces") + rootCmd.AddCommand(addUserCmd) +} diff --git a/kubectl-pg/cmd/check.go b/kubectl-pg/cmd/check.go new file mode 100644 index 000000000..266047cf0 --- /dev/null +++ b/kubectl-pg/cmd/check.go @@ -0,0 +1,72 @@ +/* +Copyright © 2019 Vineeth Pothulapati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + postgresConstants "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + apiextbeta1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "log" +) + +// checkCmd represent kubectl pg check. +var checkCmd = &cobra.Command{ + Use: "check", + Short: "Checks the Postgres operator is installed in the k8s cluster", + Long: `Checks that the Postgres CRD is registered in a k8s cluster. +This means that the operator pod was able to start normally.`, + Run: func(cmd *cobra.Command, args []string) { + check() + }, + Example: ` +kubectl pg check +`, +} + +// check validates postgresql CRD registered or not. +func check() *v1beta1.CustomResourceDefinition { + config := getConfig() + apiExtClient, err := apiextbeta1.NewForConfig(config) + if err != nil { + log.Fatal(err) + } + + crdInfo, err := apiExtClient.CustomResourceDefinitions().Get(postgresConstants.PostgresCRDResouceName, metav1.GetOptions{}) + if err != nil { + log.Fatal(err) + } + + if crdInfo.Name == postgresConstants.PostgresCRDResouceName { + fmt.Printf("Postgres Operator is installed in the k8s cluster.\n") + } else { + fmt.Printf("Postgres Operator is not installed in the k8s cluster.\n") + } + return crdInfo +} + +func init() { + rootCmd.AddCommand(checkCmd) +} diff --git a/kubectl-pg/cmd/connect.go b/kubectl-pg/cmd/connect.go new file mode 100644 index 000000000..2f1500639 --- /dev/null +++ b/kubectl-pg/cmd/connect.go @@ -0,0 +1,142 @@ +/* +Copyright © 2019 Vineeth Pothulapati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "github.com/spf13/cobra" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/remotecommand" + "log" + "os" + user "os/user" +) + +// connectCmd represents the kubectl pg connect command +var connectCmd = &cobra.Command{ + Use: "connect", + Short: "Connects to the shell prompt, psql prompt of postgres cluster", + Long: `Connects to the shell prompt, psql prompt of postgres cluster and also to specified replica or master.`, + Run: func(cmd *cobra.Command, args []string) { + clusterName, _ := cmd.Flags().GetString("cluster") + master, _ := cmd.Flags().GetBool("master") + replica, _ := cmd.Flags().GetString("replica") + psql, _ := cmd.Flags().GetBool("psql") + userName, _ := cmd.Flags().GetString("user") + dbName, _ := cmd.Flags().GetString("database") + + if psql { + if userName == "" { + userInfo, err := user.Current() + if err != nil { + log.Fatal(err) + } + userName = userInfo.Username + } + } + if dbName == "" { + dbName = userName + } + + connect(clusterName, master, replica, psql, userName, dbName) + }, + Example: ` +#connects to the master of postgres cluster +kubectl pg connect -c cluster -m + +#connects to the random replica of postgres cluster +kubectl pg connect -c cluster + +#connects to the provided replica number of postgres cluster +kubectl pg connect -c cluster -r 2 + +#connects to psql prompt of master for provided postgres cluster with current shell user +kubectl pg connect -c cluster -p -m + +#connects to psql prompt of random replica for provided postgres cluster with provided user and db +kubectl pg connect -c cluster -p -u user01 -d db01 +`, +} + +func connect(clusterName string, master bool, replica string, psql bool, user string, dbName string) { + config := getConfig() + client, er := kubernetes.NewForConfig(config) + if er != nil { + log.Fatal(er) + } + + podName := getPodName(clusterName, master, replica) + execRequest := &rest.Request{} + + if psql { + execRequest = client.CoreV1().RESTClient().Post().Resource("pods"). + Name(podName). + Namespace(getCurrentNamespace()). + SubResource("exec"). + Param("container", "postgres"). + Param("command", "psql"). + Param("command", dbName). + Param("command", user). + Param("stdin", "true"). + Param("stdout", "true"). + Param("stderr", "true"). + Param("tty", "true") + } else { + execRequest = client.CoreV1().RESTClient().Post().Resource("pods"). + Name(podName). + Namespace(getCurrentNamespace()). + SubResource("exec"). + Param("container", "postgres"). + Param("command", "su"). + Param("command", "postgres"). + Param("stdin", "true"). + Param("stdout", "true"). + Param("stderr", "true"). + Param("tty", "true") + } + + exec, err := remotecommand.NewSPDYExecutor(config, "POST", execRequest.URL()) + if err != nil { + log.Fatal(err) + } + + err = exec.Stream(remotecommand.StreamOptions{ + Stdin: os.Stdin, + Stdout: os.Stdout, + Stderr: os.Stderr, + Tty: true, + }) + if err != nil { + log.Fatal(err) + } +} + +func init() { + connectCmd.Flags().StringP("cluster", "c", "", "provide the cluster name.") + connectCmd.Flags().BoolP("master", "m", false, "connect to master.") + connectCmd.Flags().StringP("replica", "r", "", "connect to replica. Specify replica number.") + connectCmd.Flags().BoolP("psql", "p", false, "connect to psql prompt.") + connectCmd.Flags().StringP("user", "u", "", "provide user.") + connectCmd.Flags().StringP("database", "d", "", "provide database name.") + rootCmd.AddCommand(connectCmd) +} diff --git a/kubectl-pg/cmd/create.go b/kubectl-pg/cmd/create.go new file mode 100644 index 000000000..2ed7b50c0 --- /dev/null +++ b/kubectl-pg/cmd/create.go @@ -0,0 +1,76 @@ +/* +Copyright © 2019 Vineeth Pothulapati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + v1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" + PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" + "io/ioutil" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme" + "log" +) + +// createCmd kubectl pg create. +var createCmd = &cobra.Command{ + Use: "create", + Short: "Creates postgres object using manifest file", + Long: `Creates postgres custom resource objects from a manifest file.`, + Run: func(cmd *cobra.Command, args []string) { + fileName, _ := cmd.Flags().GetString("file") + create(fileName) + }, + Example: ` +kubectl pg create -f cluster-manifest.yaml +`, +} + +// Create postgresql resources. +func create(fileName string) { + config := getConfig() + postgresConfig, err := PostgresqlLister.NewForConfig(config) + ymlFile, err := ioutil.ReadFile(fileName) + if err != nil { + log.Fatal(err) + } + + decode := scheme.Codecs.UniversalDeserializer().Decode + obj, _, err := decode([]byte(ymlFile), nil, &v1.Postgresql{}) + if err != nil { + log.Fatal(err) + } + + postgresSql := obj.(*v1.Postgresql) + _, err = postgresConfig.Postgresqls(postgresSql.Namespace).Create(postgresSql) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("postgresql %s created.\n", postgresSql.Name) +} + +func init() { + createCmd.Flags().StringP("file", "f", "", "manifest file with the cluster definition.") + rootCmd.AddCommand(createCmd) +} diff --git a/kubectl-pg/cmd/delete.go b/kubectl-pg/cmd/delete.go new file mode 100644 index 000000000..3f51c2128 --- /dev/null +++ b/kubectl-pg/cmd/delete.go @@ -0,0 +1,132 @@ +/* +Copyright © 2019 Vineeth Pothulapati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" + PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" + "io/ioutil" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "log" +) + +// deleteCmd represents kubectl pg delete. +var deleteCmd = &cobra.Command{ + Use: "delete", + Short: "Deletes postgresql object by cluster-name/manifest file", + Long: `Deletes the postgres objects identified by a manifest file or cluster-name. +Deleting the manifest is sufficient to delete the cluster.`, + Run: func(cmd *cobra.Command, args []string) { + namespace, _ := cmd.Flags().GetString("namespace") + file, _ := cmd.Flags().GetString("file") + + if file != "" { + deleteByFile(file) + } else if namespace != "" { + if len(args) != 0 { + clusterName := args[0] + deleteByName(clusterName, namespace) + } else { + fmt.Println("cluster name can't be empty") + } + } else { + fmt.Println("use the flag either -n or -f to delete a resource.") + } + }, + Example: ` +#Deleting the postgres cluster using manifest file +kubectl pg delete -f cluster-manifest.yaml + +#Deleting the postgres cluster using cluster name in current namespace. +kubectl pg delete cluster01 + +#Deleting the postgres cluster using cluster name in provided namespace +kubectl pg delete cluster01 -n namespace01 +`, +} + +func deleteByFile(file string) { + config := getConfig() + postgresConfig, err := PostgresqlLister.NewForConfig(config) + if err != nil { + log.Fatal(err) + } + + ymlFile, err := ioutil.ReadFile(file) + if err != nil { + log.Fatal(err) + } + + decode := scheme.Codecs.UniversalDeserializer().Decode + obj, _, err := decode([]byte(ymlFile), nil, &v1.Postgresql{}) + if err != nil { + log.Fatal(err) + } + + postgresSql := obj.(*v1.Postgresql) + _, err = postgresConfig.Postgresqls(postgresSql.Namespace).Get(postgresSql.Name, metav1.GetOptions{}) + if err != nil { + fmt.Printf("Postgresql %s not found with the provided namespace %s : %s \n", postgresSql.Name, postgresSql.Namespace, err) + return + } + fmt.Printf("Are you sure you want to remove this PostgreSQL cluster? If so, please type (%s/%s) and hit Enter\n", postgresSql.Namespace, postgresSql.Name) + + confirmAction(postgresSql.Name, postgresSql.Namespace) + err = postgresConfig.Postgresqls(postgresSql.Namespace).Delete(postgresSql.Name, &metav1.DeleteOptions{}) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Postgresql %s deleted from %s.\n", postgresSql.Name, postgresSql.Namespace) +} + +func deleteByName(clusterName string, namespace string) { + config := getConfig() + postgresConfig, err := PostgresqlLister.NewForConfig(config) + if err != nil { + log.Fatal(err) + } + + _, err = postgresConfig.Postgresqls(namespace).Get(clusterName, metav1.GetOptions{}) + if err != nil { + fmt.Printf("Postgresql %s not found with the provided namespace %s : %s \n", clusterName, namespace, err) + return + } + fmt.Printf("Are you sure you want to remove this PostgreSQL cluster? If so, please type (%s/%s) and hit Enter\n", namespace, clusterName) + + confirmAction(clusterName, namespace) + err = postgresConfig.Postgresqls(namespace).Delete(clusterName, &metav1.DeleteOptions{}) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Postgresql %s deleted from %s.\n", clusterName, namespace) +} + +func init() { + namespace := getCurrentNamespace() + deleteCmd.Flags().StringP("namespace", "n", namespace, "namespace of the cluster to be deleted.") + deleteCmd.Flags().StringP("file", "f", "", "manifest file with the cluster definition.") + rootCmd.AddCommand(deleteCmd) +} diff --git a/kubectl-pg/cmd/extVolume.go b/kubectl-pg/cmd/extVolume.go new file mode 100644 index 000000000..351876c68 --- /dev/null +++ b/kubectl-pg/cmd/extVolume.go @@ -0,0 +1,117 @@ +/* +Copyright © 2019 Vineeth Pothulapati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "encoding/json" + "fmt" + "github.com/spf13/cobra" + PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "log" + "strconv" +) + +// extVolumeCmd represents the extVolume command +var extVolumeCmd = &cobra.Command{ + Use: "ext-volume", + Short: "Increases the volume size of a given Postgres cluster", + Long: `Extends the volume of the postgres cluster. But volume cannot be shrinked.`, + Run: func(cmd *cobra.Command, args []string) { + clusterName, _ := cmd.Flags().GetString("cluster") + if len(args) > 0 { + volume := args[0] + extVolume(volume, clusterName) + } else { + fmt.Println("please enter the cluster name with -c flag & volume in desired units") + } + }, + Example: ` +#Extending the volume size of provided cluster +kubectl pg ext-volume 2Gi -c cluster01 +`, +} + +// extend volume with provided size & cluster name +func extVolume(increasedVolumeSize string, clusterName string) { + config := getConfig() + postgresConfig, err := PostgresqlLister.NewForConfig(config) + if err != nil { + log.Fatal(err) + } + + namespace := getCurrentNamespace() + postgresql, err := postgresConfig.Postgresqls(namespace).Get(clusterName, metav1.GetOptions{}) + if err != nil { + log.Fatalf("hii %v", err) + } + + oldSize, err := resource.ParseQuantity(postgresql.Spec.Volume.Size) + if err != nil { + log.Fatal(err) + } + + newSize, err := resource.ParseQuantity(increasedVolumeSize) + if err != nil { + log.Fatal(err) + } + + _, err = strconv.Atoi(newSize.String()) + if err == nil { + fmt.Println("provide the valid volume size with respective units i.e Ki, Mi, Gi") + return + } + + if newSize.Value() > oldSize.Value() { + patchInstances := volumePatch(newSize) + response, err := postgresConfig.Postgresqls(namespace).Patch(postgresql.Name, types.MergePatchType, patchInstances, "") + if err != nil { + log.Fatal(err) + } + if postgresql.ResourceVersion != response.ResourceVersion { + fmt.Printf("%s volume is extended to %s.\n", response.Name, increasedVolumeSize) + } else { + fmt.Printf("%s volume %s is unchanged.\n", response.Name, postgresql.Spec.Volume.Size) + } + } else if newSize.Value() == oldSize.Value() { + fmt.Println("volume already has the desired size.") + } else { + fmt.Printf("volume %s size cannot be shrinked.\n", postgresql.Spec.Volume.Size) + } +} + +func volumePatch(volume resource.Quantity) []byte { + patchData := map[string]map[string]map[string]resource.Quantity{"spec": {"volume": {"size": volume}}} + patch, err := json.Marshal(patchData) + if err != nil { + log.Fatal(err, "unable to parse patch to extend volume") + } + return patch +} + +func init() { + extVolumeCmd.Flags().StringP("cluster", "c", "", "provide cluster name.") + rootCmd.AddCommand(extVolumeCmd) +} diff --git a/kubectl-pg/cmd/list.go b/kubectl-pg/cmd/list.go new file mode 100644 index 000000000..df827ffaf --- /dev/null +++ b/kubectl-pg/cmd/list.go @@ -0,0 +1,116 @@ +/* +Copyright © 2019 Vineeth Pothulapati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" + PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "log" + "strconv" + "time" +) + +const ( + TrimCreateTimestamp = 6000000000 +) + +// listCmd represents kubectl pg list. +var listCmd = &cobra.Command{ + Use: "list", + Short: "Lists all the resources of kind postgresql", + Long: `Lists all the info specific to postgresql objects.`, + Run: func(cmd *cobra.Command, args []string) { + allNamespaces, _ := cmd.Flags().GetBool("all-namespaces") + namespace, _ := cmd.Flags().GetString("namespace") + if allNamespaces { + list(allNamespaces, "") + } else { + list(allNamespaces, namespace) + } + + }, + Example: ` +#Lists postgres cluster in current namespace +kubectl pg list + +#Lists postgres clusters in all namespaces +kubectl pg list -A +`, +} + +// list command to list postgres. +func list(allNamespaces bool, namespace string) { + config := getConfig() + postgresConfig, err := PostgresqlLister.NewForConfig(config) + if err != nil { + log.Fatal(err) + } + + var listPostgres *v1.PostgresqlList + listPostgres, err = postgresConfig.Postgresqls(namespace).List(metav1.ListOptions{}) + if err != nil { + log.Fatal(err) + } + + if len(listPostgres.Items) == 0 { + if namespace != "" { + fmt.Printf("No Postgresql clusters found in namespace: %v\n", namespace) + } else { + fmt.Println("No Postgresql clusters found in all namespaces") + } + return + } + + if allNamespaces { + listAll(listPostgres) + } else { + listWithNamespace(listPostgres) + } +} + +func listAll(listPostgres *v1.PostgresqlList) { + template := "%-32s%-16s%-12s%-12s%-12s%-12s%-12s\n" + fmt.Printf(template, "NAME", "STATUS", "INSTANCES", "VERSION", "AGE", "VOLUME", "NAMESPACE") + for _, pgObjs := range listPostgres.Items { + fmt.Printf(template, pgObjs.Name, pgObjs.Status.PostgresClusterStatus, strconv.Itoa(int(pgObjs.Spec.NumberOfInstances)), + pgObjs.Spec.PgVersion, time.Since(pgObjs.CreationTimestamp.Time).Truncate(TrimCreateTimestamp), pgObjs.Spec.Size, pgObjs.Namespace) + } +} + +func listWithNamespace(listPostgres *v1.PostgresqlList) { + template := "%-32s%-16s%-12s%-12s%-12s%-12s\n" + fmt.Printf(template, "NAME", "STATUS", "INSTANCES", "VERSION", "AGE", "VOLUME") + for _, pgObjs := range listPostgres.Items { + fmt.Printf(template, pgObjs.Name, pgObjs.Status.PostgresClusterStatus, strconv.Itoa(int(pgObjs.Spec.NumberOfInstances)), + pgObjs.Spec.PgVersion, time.Since(pgObjs.CreationTimestamp.Time).Truncate(TrimCreateTimestamp), pgObjs.Spec.Size) + } +} + +func init() { + listCmd.Flags().BoolP("all-namespaces", "A", false, "list pg resources across all namespaces.") + listCmd.Flags().StringP("namespace", "n", getCurrentNamespace(), "provide the namespace") + rootCmd.AddCommand(listCmd) +} diff --git a/kubectl-pg/cmd/logs.go b/kubectl-pg/cmd/logs.go new file mode 100644 index 000000000..b527b6c52 --- /dev/null +++ b/kubectl-pg/cmd/logs.go @@ -0,0 +1,141 @@ +/* +Copyright © 2019 Vineeth Pothulapati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "github.com/spf13/cobra" + "io" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "log" + "os" +) + +// logsCmd represents the logs command +var logsCmd = &cobra.Command{ + Use: "logs", + Short: "This will fetch the logs of the specified postgres cluster & postgres operator", + Long: `Fetches the logs of the postgres cluster (i.e master( with -m flag) & replica with (-r 1 pod number) and without -m or -r connects to random replica`, + Run: func(cmd *cobra.Command, args []string) { + opLogs, _ := cmd.Flags().GetBool("operator") + clusterName, _ := cmd.Flags().GetString("cluster") + master, _ := cmd.Flags().GetBool("master") + replica, _ := cmd.Flags().GetString("replica") + + if opLogs { + operatorLogs() + } else { + clusterLogs(clusterName, master, replica) + } + }, + Example: ` +#Fetch the logs of the postgres operator +kubectl pg logs -o + +#Fetch the logs of the master for provided cluster +kubectl pg logs -c cluster01 -m + +#Fetch the logs of the random replica for provided cluster +kubectl pg logs -c cluster01 + +#Fetch the logs of the provided replica number of the cluster +kubectl pg logs -c cluster01 -r 3 +`, +} + +func operatorLogs() { + config := getConfig() + client, err := kubernetes.NewForConfig(config) + if err != nil { + log.Fatal(err) + } + + operator := getPostgresOperator(client) + allPods, err := client.CoreV1().Pods(operator.Namespace).List(metav1.ListOptions{}) + if err != nil { + log.Fatal(err) + } + + var operatorPodName string + for _, pod := range allPods.Items { + for key, value := range pod.Labels { + if (key == "name" && value == OperatorName) || (key == "app.kubernetes.io/name" && value == OperatorName) { + operatorPodName = pod.Name + break + } + } + } + + execRequest := client.CoreV1().RESTClient().Get().Namespace(operator.Namespace). + Name(operatorPodName). + Resource("pods"). + SubResource("log"). + Param("follow", "--follow"). + Param("container", OperatorName) + + readCloser, err := execRequest.Stream() + if err != nil { + log.Fatal(err) + } + + defer readCloser.Close() + _, err = io.Copy(os.Stdout, readCloser) + if err != nil { + log.Fatal(err) + } +} + +func clusterLogs(clusterName string, master bool, replica string) { + config := getConfig() + client, err := kubernetes.NewForConfig(config) + if err != nil { + log.Fatal(err) + } + + podName := getPodName(clusterName, master, replica) + execRequest := client.CoreV1().RESTClient().Get().Namespace(getCurrentNamespace()). + Name(podName). + Resource("pods"). + SubResource("log"). + Param("follow", "--follow"). + Param("container", "postgres") + + readCloser, err := execRequest.Stream() + if err != nil { + log.Fatal(err) + } + + defer readCloser.Close() + _, err = io.Copy(os.Stdout, readCloser) + if err != nil { + log.Fatal(err) + } +} + +func init() { + rootCmd.AddCommand(logsCmd) + logsCmd.Flags().BoolP("operator", "o", false, "logs of operator") + logsCmd.Flags().StringP("cluster", "c", "", "logs for the provided cluster") + logsCmd.Flags().BoolP("master", "m", false, "Patroni logs of master") + logsCmd.Flags().StringP("replica", "r", "", "Patroni logs of replica. Specify replica number.") +} diff --git a/kubectl-pg/cmd/root.go b/kubectl-pg/cmd/root.go new file mode 100644 index 000000000..916852998 --- /dev/null +++ b/kubectl-pg/cmd/root.go @@ -0,0 +1,50 @@ +/* +Copyright © 2019 Vineeth Pothulapati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "os" +) + +var rootCmd = &cobra.Command{ + Use: "kubectl-pg", + Short: "kubectl plugin for the Zalando Postgres operator.", + Long: `kubectl pg plugin for interaction with Zalando postgres operator.`, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + viper.SetDefault("author", "Vineeth Pothulapati ") + viper.SetDefault("license", "mit") +} diff --git a/kubectl-pg/cmd/scale.go b/kubectl-pg/cmd/scale.go new file mode 100644 index 000000000..d273bbd6f --- /dev/null +++ b/kubectl-pg/cmd/scale.go @@ -0,0 +1,191 @@ +/* +Copyright © 2019 Vineeth Pothulapati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "encoding/json" + "fmt" + "github.com/spf13/cobra" + PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" + v1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "log" + "strconv" +) + +// scaleCmd represents the scale command +var scaleCmd = &cobra.Command{ + Use: "scale", + Short: "Add/remove pods to a Postgres cluster", + Long: `Scales the postgres objects using cluster-name. +Scaling to 0 leads to down time.`, + Run: func(cmd *cobra.Command, args []string) { + clusterName, err := cmd.Flags().GetString("cluster") + namespace, err := cmd.Flags().GetString("namespace") + if err != nil { + log.Fatal(err) + } + + if len(args) > 0 { + numberOfInstances, err := strconv.Atoi(args[0]) + if err != nil { + log.Fatal(err) + } + scale(int32(numberOfInstances), clusterName, namespace) + } else { + fmt.Println("Please enter number of instances to scale.") + } + + }, + Example: ` +#Usage +kubectl pg scale [NUMBER-OF-INSTANCES] -c [CLUSTER-NAME] -n [NAMESPACE] + +#Scales the number of instances of the provided cluster +kubectl pg scale 5 -c cluster01 +`, +} + +func scale(numberOfInstances int32, clusterName string, namespace string) { + config := getConfig() + postgresConfig, err := PostgresqlLister.NewForConfig(config) + if err != nil { + log.Fatal(err) + } + + postgresql, err := postgresConfig.Postgresqls(namespace).Get(clusterName, metav1.GetOptions{}) + if err != nil { + log.Fatal(err) + } + + minInstances, maxInstances := allowedMinMaxInstances(config) + + if minInstances == -1 && maxInstances == -1 { + postgresql.Spec.NumberOfInstances = numberOfInstances + } else if numberOfInstances <= maxInstances && numberOfInstances >= minInstances { + postgresql.Spec.NumberOfInstances = numberOfInstances + } else if minInstances == -1 && numberOfInstances < postgresql.Spec.NumberOfInstances || + maxInstances == -1 && numberOfInstances > postgresql.Spec.NumberOfInstances { + postgresql.Spec.NumberOfInstances = numberOfInstances + } else { + log.Fatalf("cannot scale to the provided instances as they don't adhere to MIN_INSTANCES: %v and MAX_INSTANCES: %v provided in configmap or operatorconfiguration", maxInstances, minInstances) + } + + if numberOfInstances == 0 { + fmt.Printf("Scaling to zero leads to down time. please type %s/%s and hit Enter this serves to confirm the action\n", namespace, clusterName) + confirmAction(clusterName, namespace) + } + + patchInstances := scalePatch(numberOfInstances) + UpdatedPostgres, err := postgresConfig.Postgresqls(namespace).Patch(postgresql.Name, types.MergePatchType, patchInstances, "") + if err != nil { + log.Fatal(err) + } + + if UpdatedPostgres.ResourceVersion != postgresql.ResourceVersion { + fmt.Printf("scaled postgresql %s/%s to %d instances\n", UpdatedPostgres.Namespace, UpdatedPostgres.Name, UpdatedPostgres.Spec.NumberOfInstances) + return + } + fmt.Printf("postgresql %s is unchanged.\n", postgresql.Name) +} + +func scalePatch(value int32) []byte { + instances := map[string]map[string]int32{"spec": {"numberOfInstances": value}} + patchInstances, err := json.Marshal(instances) + if err != nil { + log.Fatal(err, "unable to parse patch for scale") + } + return patchInstances +} + +func allowedMinMaxInstances(config *rest.Config) (int32, int32) { + k8sClient, err := kubernetes.NewForConfig(config) + if err != nil { + log.Fatal(err) + } + + var operator *v1.Deployment + operator = getPostgresOperator(k8sClient) + + operatorContainer := operator.Spec.Template.Spec.Containers + var configMapName, operatorConfigName string + // -1 indicates no limitations for min/max instances + minInstances := -1 + maxInstances := -1 + for _, envData := range operatorContainer[0].Env { + if envData.Name == "CONFIG_MAP_NAME" { + configMapName = envData.Value + } + if envData.Name == "POSTGRES_OPERATOR_CONFIGURATION_OBJECT" { + operatorConfigName = envData.Value + } + } + + if operatorConfigName == "" { + configMap, err := k8sClient.CoreV1().ConfigMaps(operator.Namespace).Get(configMapName, metav1.GetOptions{}) + if err != nil { + log.Fatal(err) + } + + configMapData := configMap.Data + for key, value := range configMapData { + if key == "min_instances" { + minInstances, err = strconv.Atoi(value) + if err != nil { + log.Fatalf("invalid min instances in configmap %v", err) + } + } + + if key == "max_instances" { + maxInstances, err = strconv.Atoi(value) + if err != nil { + log.Fatalf("invalid max instances in configmap %v", err) + } + } + } + } else if configMapName == "" { + pgClient, err := PostgresqlLister.NewForConfig(config) + if err != nil { + log.Fatal(err) + } + + operatorConfig, err := pgClient.OperatorConfigurations(operator.Namespace).Get(operatorConfigName, metav1.GetOptions{}) + if err != nil { + log.Fatalf("unable to read operator configuration %v", err) + } + + minInstances = int(operatorConfig.Configuration.MinInstances) + maxInstances = int(operatorConfig.Configuration.MaxInstances) + } + return int32(minInstances), int32(maxInstances) +} + +func init() { + namespace := getCurrentNamespace() + scaleCmd.Flags().StringP("namespace", "n", namespace, "namespace of the cluster to be scaled") + scaleCmd.Flags().StringP("cluster", "c", "", "provide the cluster name.") + rootCmd.AddCommand(scaleCmd) +} diff --git a/kubectl-pg/cmd/update.go b/kubectl-pg/cmd/update.go new file mode 100644 index 000000000..6efdb8b8c --- /dev/null +++ b/kubectl-pg/cmd/update.go @@ -0,0 +1,91 @@ +/* +Copyright © 2019 Vineeth Pothulapati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + v1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" + PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" + "io/ioutil" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "log" +) + +// updateCmd represents kubectl pg update +var updateCmd = &cobra.Command{ + Use: "update", + Short: "Updates postgresql object using manifest file", + Long: `Updates the state of cluster using manifest file to reflect the changes on the cluster.`, + Run: func(cmd *cobra.Command, args []string) { + fileName, _ := cmd.Flags().GetString("file") + updatePgResources(fileName) + }, + Example: ` +#usage +kubectl pg update -f [File-NAME] + +#update the postgres cluster with updated manifest file +kubectl pg update -f cluster-manifest.yaml +`, +} + +// Update postgresql resources. +func updatePgResources(fileName string) { + config := getConfig() + postgresConfig, err := PostgresqlLister.NewForConfig(config) + ymlFile, err := ioutil.ReadFile(fileName) + if err != nil { + log.Fatal(err) + } + + decode := scheme.Codecs.UniversalDeserializer().Decode + obj, _, err := decode([]byte(ymlFile), nil, &v1.Postgresql{}) + if err != nil { + log.Fatal(err) + } + + newPostgresObj := obj.(*v1.Postgresql) + oldPostgresObj, err := postgresConfig.Postgresqls(newPostgresObj.Namespace).Get(newPostgresObj.Name, metav1.GetOptions{}) + if err != nil { + log.Fatal(err) + } + + newPostgresObj.ResourceVersion = oldPostgresObj.ResourceVersion + response, err := postgresConfig.Postgresqls(newPostgresObj.Namespace).Update(newPostgresObj) + if err != nil { + log.Fatal(err) + } + + if newPostgresObj.ResourceVersion != response.ResourceVersion { + fmt.Printf("postgresql %s updated.\n", response.Name) + } else { + fmt.Printf("postgresql %s is unchanged.\n", response.Name) + } +} + +func init() { + updateCmd.Flags().StringP("file", "f", "", "manifest file with the cluster definition.") + rootCmd.AddCommand(updateCmd) +} diff --git a/kubectl-pg/cmd/util.go b/kubectl-pg/cmd/util.go new file mode 100644 index 000000000..329f9a28f --- /dev/null +++ b/kubectl-pg/cmd/util.go @@ -0,0 +1,170 @@ +/* +Copyright © 2019 Vineeth Pothulapati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "flag" + "fmt" + PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" + v1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/homedir" + "log" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" +) + +const ( + OperatorName = "postgres-operator" + DefaultNamespace = "default" +) + +func getConfig() *restclient.Config { + var kubeconfig *string + var config *restclient.Config + envKube := os.Getenv("KUBECONFIG") + if envKube != "" { + kubeconfig = &envKube + } else { + if home := homedir.HomeDir(); home != "" { + kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file") + } else { + kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file") + } + } + flag.Parse() + var err error + config, err = clientcmd.BuildConfigFromFlags("", *kubeconfig) + if err != nil { + log.Fatal(err) + } + return config +} + +func getCurrentNamespace() string { + namespace, err := exec.Command("kubectl", "config", "view", "--minify", "--output", "jsonpath={..namespace}").CombinedOutput() + if err != nil { + log.Fatal(err) + } + currentNamespace := string(namespace) + if currentNamespace == "" { + currentNamespace = DefaultNamespace + } + return currentNamespace +} + +func confirmAction(clusterName string, namespace string) { + for { + confirmClusterDetails := "" + _, err := fmt.Scan(&confirmClusterDetails) + if err != nil { + log.Fatalf("couldn't get confirmation from the user %v", err) + } + clusterDetails := strings.Split(confirmClusterDetails, "/") + if clusterDetails[0] != namespace || clusterDetails[1] != clusterName { + fmt.Printf("cluster name or namespace doesn't match. Please re-enter %s/%s\nHint: Press (ctrl+c) to exit\n", namespace, clusterName) + } else { + return + } + } +} + +func getPodName(clusterName string, master bool, replicaNumber string) string { + config := getConfig() + client, er := kubernetes.NewForConfig(config) + if er != nil { + log.Fatal(er) + } + + postgresConfig, err := PostgresqlLister.NewForConfig(config) + if err != nil { + log.Fatal(err) + } + + postgresCluster, err := postgresConfig.Postgresqls(getCurrentNamespace()).Get(clusterName, metav1.GetOptions{}) + if err != nil { + log.Fatal(err) + } + + numOfInstances := postgresCluster.Spec.NumberOfInstances + var podName string + var podRole string + replica := clusterName + "-" + replicaNumber + + for ins := 0; ins < int(numOfInstances); ins++ { + pod, err := client.CoreV1().Pods(getCurrentNamespace()).Get(clusterName+"-"+strconv.Itoa(ins), metav1.GetOptions{}) + if err != nil { + log.Fatal(err) + } + + podRole = pod.Labels["spilo-role"] + if podRole == "master" && master { + podName = pod.Name + fmt.Printf("connected to %s with pod name as %s\n", podRole, podName) + break + } else if podRole == "replica" && !master && (pod.Name == replica || replicaNumber == "") { + podName = pod.Name + fmt.Printf("connected to %s with pod name as %s\n", podRole, podName) + break + } + } + if podName == "" { + log.Fatal("Provided replica doesn't exist") + } + return podName +} + +func getPostgresOperator(k8sClient *kubernetes.Clientset) *v1.Deployment { + var operator *v1.Deployment + operator, err := k8sClient.AppsV1().Deployments(getCurrentNamespace()).Get(OperatorName, metav1.GetOptions{}) + if err == nil { + return operator + } + + allDeployments := k8sClient.AppsV1().Deployments("") + listDeployments, err := allDeployments.List(metav1.ListOptions{}) + if err != nil { + log.Fatal(err) + } + + for _, deployment := range listDeployments.Items { + if deployment.Name == OperatorName { + operator = deployment.DeepCopy() + break + } else { + for key, value := range deployment.Labels { + if key == "app.kubernetes.io/name" && value == OperatorName { + operator = deployment.DeepCopy() + break + } + } + } + } + return operator +} diff --git a/kubectl-pg/cmd/version.go b/kubectl-pg/cmd/version.go new file mode 100644 index 000000000..91976fbc2 --- /dev/null +++ b/kubectl-pg/cmd/version.go @@ -0,0 +1,79 @@ +/* +Copyright © 2019 Vineeth Pothulapati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "k8s.io/client-go/kubernetes" + "log" + "strings" +) + +var KubectlPgVersion string = "1.0" + +// versionCmd represents the version command +var versionCmd = &cobra.Command{ + Use: "version", + Short: "version of kubectl-pg & postgres-operator", + Long: `version of kubectl-pg and current running postgres-operator`, + Run: func(cmd *cobra.Command, args []string) { + namespace, err := cmd.Flags().GetString("namespace") + if err != nil { + log.Fatal(err) + } + version(namespace) + }, + Example: ` +#Lists the version of kubectl pg plugin and postgres operator in current namespace +kubectl pg version + +#Lists the version of kubectl pg plugin and postgres operator in provided namespace +kubectl pg version -n namespace01 +`, +} + +func version(namespace string) { + fmt.Printf("kubectl-pg: %s\n", KubectlPgVersion) + + config := getConfig() + client, err := kubernetes.NewForConfig(config) + if err != nil { + log.Fatal(err) + } + + operatorDeployment := getPostgresOperator(client) + if operatorDeployment.Name == "" { + log.Fatal("make sure zalando's postgres operator is running") + } + operatorImage := operatorDeployment.Spec.Template.Spec.Containers[0].Image + imageDetails := strings.Split(operatorImage, ":") + imageSplit := len(imageDetails) + imageVersion := imageDetails[imageSplit-1] + fmt.Printf("Postgres-Operator: %s\n", imageVersion) +} + +func init() { + rootCmd.AddCommand(versionCmd) + versionCmd.Flags().StringP("namespace", "n", DefaultNamespace, "provide the namespace.") +} diff --git a/kubectl-pg/go.mod b/kubectl-pg/go.mod new file mode 100644 index 000000000..bf1591beb --- /dev/null +++ b/kubectl-pg/go.mod @@ -0,0 +1,14 @@ +module kubectl-pg + +go 1.12 + +require ( + github.com/imdario/mergo v0.3.7 // indirect + github.com/kisielk/errcheck v1.2.0 // indirect + github.com/spf13/cobra v0.0.5 + github.com/spf13/viper v1.4.0 + github.com/zalando/postgres-operator v1.2.0 + k8s.io/apiextensions-apiserver v0.0.0-20190726024412-102230e288fd + k8s.io/apimachinery v0.0.0-20190727130956-f97a4e5b4abc + k8s.io/client-go v0.0.0-20190726023111-a9c895e7f2ac +) diff --git a/kubectl-pg/go.sum b/kubectl-pg/go.sum new file mode 100644 index 000000000..81ace7f42 --- /dev/null +++ b/kubectl-pg/go.sum @@ -0,0 +1,386 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/VineethReddy02/postgres-operator v1.1.0 h1:I1CyYLrPbI78blfQY5Dy7m9TNsVVvCNXSM9rEKh888Q= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.1-coreos.6/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v0.0.0-20180117170138-065b426bd416/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= +github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= +github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= +github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= +github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= +github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= +github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= +github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= +github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= +github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= +github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= +github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v0.0.0-20190410021324-65acae22fc9/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v0.0.0-20170330212424-2500245aa611/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.3.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= +github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.4/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/zalando/postgres-operator v1.2.0 h1:XV3zM2iON4O8iqLTlSNeekxIqistnUx7Btfk2w7mDaY= +github.com/zalando/postgres-operator v1.2.0/go.mod h1:0+dT6DbKj6yvytwBpApmSwEbMBqbLS9AzgUZacbG0lY= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.uber.org/atomic v0.0.0-20181018215023-8dc6146f7569/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v0.0.0-20180814183419-67bc79d13d15/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 h1:1wopBVtVdWnn03fZelqdXTqk7U7zPQCb+T4rbU9ZEoU= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v9p/3ea4Rz+nnM5K/i4= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20170731182057-09f6ed296fc6/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.13.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o= +gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.0.0-20190620084959-7cf5895f2711/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A= +k8s.io/api v0.0.0-20190726022912-69e1bce1dad5 h1:vSfC/FjyeuqXC/fjdNqZixNpeec4mEHJ68K3kzetm/M= +k8s.io/api v0.0.0-20190726022912-69e1bce1dad5/go.mod h1:V6cpJ9D7WqSy0wqcE096gcbj+W//rshgQgmj1Shdwi8= +k8s.io/apiextensions-apiserver v0.0.0-20190726024412-102230e288fd h1:qnJFeJfmqE4nGI+xUjLsSzpl5o08JkRGttSIfzfqj7U= +k8s.io/apiextensions-apiserver v0.0.0-20190726024412-102230e288fd/go.mod h1:sDyIzs1dBO19o8gtqZK79kPQ+OIyjo34y2Gh2O+2MMo= +k8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA= +k8s.io/apimachinery v0.0.0-20190726022757-641a75999153/go.mod h1:eXR4ljjmbwK6Ng0PKsXRySPXnTUy/qBUa6kPDeckhQ0= +k8s.io/apimachinery v0.0.0-20190727130956-f97a4e5b4abc h1:fi1vG9UrnqoGU/H2HP2rr7GH6vaQeFdLxfocg5uMQmA= +k8s.io/apimachinery v0.0.0-20190727130956-f97a4e5b4abc/go.mod h1:eXR4ljjmbwK6Ng0PKsXRySPXnTUy/qBUa6kPDeckhQ0= +k8s.io/apiserver v0.0.0-20190726023815-781c3cd1b3dc/go.mod h1:Gy8ElOsvjzEZF7lUFUffGBuA6Vg4qsN/r+vt05szn6c= +k8s.io/client-go v0.0.0-20190620085101-78d2af792bab h1:E8Fecph0qbNsAbijJJQryKu4Oi9QTp5cVpjTE+nqg6g= +k8s.io/client-go v0.0.0-20190620085101-78d2af792bab/go.mod h1:E95RaSlHr79aHaX0aGSwcPNfygDiPKOVXdmivCIZT0k= +k8s.io/client-go v0.0.0-20190726023111-a9c895e7f2ac h1:wqRgq2VyWMCJW9mU9MIMAPj5jBOjFFQYbT/DydDUo94= +k8s.io/client-go v0.0.0-20190726023111-a9c895e7f2ac/go.mod h1:ncT9fCvHnM5BUiZs0RCf9vAEqRrRoJtR2sZ2evompEU= +k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o= +k8s.io/client-go v11.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= +k8s.io/code-generator v0.0.0-20190726022633-14ba7d03f06f/go.mod h1:kr7tMYxZEaP3mrijPwXnhxOvPyqdJw6TZH87KfFboQ0= +k8s.io/component-base v0.0.0-20190726023549-042c00bc1f9e/go.mod h1:KiJFR5KR5yaKNXFgCliO2CPcmAI6hdZCcb5XZyl0EhQ= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.1 h1:RVgyDHY/kFKtLqh67NvEWIgkMneNoIrdkN0CxDSQc68= +k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= +k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4= +k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= +k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a h1:2jUDc9gJja832Ftp+QbDV0tVhQHMISFn01els+2ZAcw= +k8s.io/utils v0.0.0-20190607212802-c55fbcfc754a/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/structured-merge-diff v0.0.0-20190719182312-e94e05bfbbe3/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/kubectl-pg/main.go b/kubectl-pg/main.go new file mode 100644 index 000000000..fb4ad3d5e --- /dev/null +++ b/kubectl-pg/main.go @@ -0,0 +1,31 @@ +/* +Copyright © 2019 Vineeth Pothulapati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package main + +import ( + "kubectl-pg/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/manifests/complete-postgres-manifest.yaml b/manifests/complete-postgres-manifest.yaml index e4a4acf73..cf450ef94 100644 --- a/manifests/complete-postgres-manifest.yaml +++ b/manifests/complete-postgres-manifest.yaml @@ -2,38 +2,41 @@ apiVersion: "acid.zalan.do/v1" kind: postgresql metadata: name: acid-test-cluster +# labels: +# environment: demo spec: - dockerImage: registry.opensource.zalan.do/acid/spilo-11:1.5-p9 - initContainers: - - name: date - image: busybox - command: [ "/bin/date" ] - teamId: "ACID" + dockerImage: registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p16 + teamId: "acid" volume: size: 1Gi -# storageClass: my-sc +# storageClass: my-sc numberOfInstances: 2 users: # Application/Robot users zalando: - superuser - createdb - enableMasterLoadBalancer: true - enableReplicaLoadBalancer: true + enableMasterLoadBalancer: false + enableReplicaLoadBalancer: false allowedSourceRanges: # load balancers' source ranges for both master and replica services - 127.0.0.1/32 databases: foo: zalando - -# Expert section - - enableShmVolume: true -# spiloFSGroup: 103 postgresql: - version: "10" - parameters: + version: "11" + parameters: # Expert section shared_buffers: "32MB" max_connections: "10" log_statement: "all" + + enableShmVolume: true +# spiloFSGroup: 103 +# podAnnotations: +# annotation.key: value +# podPriorityClassName: "spilo-pod-priority" +# tolerations: +# - key: postgres +# operator: Exists +# effect: NoSchedule resources: requests: cpu: 10m @@ -49,42 +52,49 @@ spec: pg_hba: - hostssl all all 0.0.0.0/0 md5 - host all all 0.0.0.0/0 md5 -# slots: -# - permanent_physical_1: -# type: physical -# - permanent_logical_1: -# type: logical -# database: foo -# plugin: pgoutput +# slots: +# permanent_physical_1: +# type: physical +# permanent_logical_1: +# type: logical +# database: foo +# plugin: pgoutput ttl: 30 loop_wait: &loop_wait 10 retry_timeout: 10 maximum_lag_on_failover: 33554432 + # restore a Postgres DB with point-in-time-recovery # with a non-empty timestamp, clone from an S3 bucket using the latest backup before the timestamp # with an empty/absent timestamp, clone from an existing alive cluster using pg_basebackup -# clone: -# uid: "efd12e58-5786-11e8-b5a7-06148230260c" -# cluster: "acid-batman" -# timestamp: "2017-12-19T12:40:33+01:00" # timezone required (offset relative to UTC, see RFC 3339 section 5.6) -# s3_wal_path: "s3://custom/path/to/bucket" +# clone: +# uid: "efd12e58-5786-11e8-b5a7-06148230260c" +# cluster: "acid-batman" +# timestamp: "2017-12-19T12:40:33+01:00" # timezone required (offset relative to UTC, see RFC 3339 section 5.6) +# s3_wal_path: "s3://custom/path/to/bucket" # run periodic backups with k8s cron jobs -# enableLogicalBackup: true -# logicalBackupSchedule: "30 00 * * *" - maintenanceWindows: - - 01:00-06:00 #UTC - - Sat:00:00-04:00 -# sidecars: -# - name: "telegraf-sidecar" -# image: "telegraf:latest" -# resources: -# limits: -# cpu: 500m -# memory: 500Mi -# requests: -# cpu: 100m -# memory: 100Mi +# enableLogicalBackup: true +# logicalBackupSchedule: "30 00 * * *" + +# maintenanceWindows: +# - 01:00-06:00 #UTC +# - Sat:00:00-04:00 + + initContainers: + - name: date + image: busybox + command: [ "/bin/date" ] +# sidecars: +# - name: "telegraf-sidecar" +# image: "telegraf:latest" +# resources: +# limits: +# cpu: 500m +# memory: 500Mi +# requests: +# cpu: 100m +# memory: 100Mi # env: # - name: "USEFUL_VAR" # value: "perhaps-true" diff --git a/manifests/configmap.yaml b/manifests/configmap.yaml index 6e92c7225..afb5957da 100644 --- a/manifests/configmap.yaml +++ b/manifests/configmap.yaml @@ -11,33 +11,40 @@ data: cluster_history_entries: "1000" cluster_labels: application:spilo cluster_name_label: version - # custom_service_annotations: - # "keyx:valuez,keya:valuea" + # custom_service_annotations: "keyx:valuez,keya:valuea" + # custom_pod_annotations: "keya:valuea,keyb:valueb" db_hosted_zone: db.example.com debug_logging: "true" # default_cpu_limit: "3" # default_cpu_request: 100m # default_memory_limit: 1Gi # default_memory_request: 100Mi - docker_image: registry.opensource.zalan.do/acid/spilo-11:1.5-p9 + docker_image: registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p16 # enable_admin_role_for_users: "true" + # enable_crd_validation: "true" # enable_database_access: "true" - enable_master_load_balancer: "true" + # enable_init_containers: "true" + enable_master_load_balancer: "false" # enable_pod_antiaffinity: "false" # enable_pod_disruption_budget: "true" enable_replica_load_balancer: "false" # enable_shm_volume: "true" + # enable_sidecars: "true" # enable_team_superuser: "false" enable_teams_api: "false" # etcd_host: "" # infrastructure_roles_secret_name: postgresql-infrastructure-roles - # inherited_labels: "" + # inherited_labels: application,environment # kube_iam_role: "" # log_s3_bucket: "" # logical_backup_docker_image: "registry.opensource.zalan.do/acid/logical-backup" + # logical_backup_s3_access_key_id: "" # logical_backup_s3_bucket: "my-bucket-url" + # logical_backup_s3_endpoint: "" + # logical_backup_s3_secret_access_key: "" + # logical_backup_s3_sse: "AES256" # logical_backup_schedule: "30 00 * * *" - master_dns_name_format: '{cluster}.{team}.staging.{hostedzone}' + master_dns_name_format: "{cluster}.{team}.{hostedzone}" # master_pod_move_timeout: 10m # max_instances: "-1" # min_instances: "-1" @@ -60,13 +67,13 @@ data: ready_wait_interval: 3s ready_wait_timeout: 30s repair_period: 5m - replica_dns_name_format: '{cluster}-repl.{team}.staging.{hostedzone}' + replica_dns_name_format: "{cluster}-repl.{team}.{hostedzone}" replication_username: standby resource_check_interval: 3s resource_check_timeout: 10m - resync_period: 5m + resync_period: 30m ring_log_lines: "100" - secret_name_template: '{username}.{cluster}.credentials' + secret_name_template: "{username}.{cluster}.credentials" # sidecar_docker_images: "" # set_memory_request_to_limit: "false" spilo_privileged: "false" diff --git a/manifests/minimal-postgres-manifest.yaml b/manifests/minimal-postgres-manifest.yaml index e94314841..75dfdf07f 100644 --- a/manifests/minimal-postgres-manifest.yaml +++ b/manifests/minimal-postgres-manifest.yaml @@ -4,7 +4,7 @@ metadata: name: acid-minimal-cluster namespace: default spec: - teamId: "ACID" + teamId: "acid" volume: size: 1Gi numberOfInstances: 2 @@ -16,4 +16,4 @@ spec: databases: foo: zalando # dbname: owner postgresql: - version: "10" + version: "11" diff --git a/manifests/operator-service-account-rbac.yaml b/manifests/operator-service-account-rbac.yaml index bca1128f4..a37abe476 100644 --- a/manifests/operator-service-account-rbac.yaml +++ b/manifests/operator-service-account-rbac.yaml @@ -10,6 +10,7 @@ kind: ClusterRole metadata: name: zalando-postgres-operator rules: +# all verbs allowed for custom operator resources - apiGroups: - acid.zalan.do resources: @@ -18,6 +19,7 @@ rules: - operatorconfigurations verbs: - "*" +# to create or get/update CRDs when starting up - apiGroups: - apiextensions.k8s.io resources: @@ -27,12 +29,14 @@ rules: - get - patch - update +# to read configuration from ConfigMaps - apiGroups: - "" resources: - configmaps verbs: - get +# to manage endpoints which are also used by Patroni - apiGroups: - "" resources: @@ -45,6 +49,7 @@ rules: - list - patch - watch # needed if zalando-postgres-operator account is used for pods as well +# to CRUD secrets for database access - apiGroups: - "" resources: @@ -54,6 +59,7 @@ rules: - update - delete - get +# to check nodes for node readiness label - apiGroups: - "" resources: @@ -62,6 +68,7 @@ rules: - get - list - watch +# to read or delete existing PVCs. Creation via StatefulSet - apiGroups: - "" resources: @@ -70,6 +77,7 @@ rules: - delete - get - list + # to read existing PVs. Creation should be done via dynamic provisioning - apiGroups: - "" resources: @@ -78,6 +86,7 @@ rules: - get - list - update # only for resizing AWS volumes +# to watch Spilo pods and do rolling updates. Creation via StatefulSet - apiGroups: - "" resources: @@ -88,6 +97,14 @@ rules: - list - watch - patch +# to resize the filesystem in Spilo pods when increasing volume size +- apiGroups: + - "" + resources: + - pods/exec + verbs: + - create +# to CRUD services to point to Postgres cluster instances - apiGroups: - "" resources: @@ -97,6 +114,7 @@ rules: - delete - get - patch +# to CRUD the StatefulSet which controls the Postgres cluster instances - apiGroups: - apps resources: @@ -107,12 +125,14 @@ rules: - get - list - patch +# to get namespaces operator resources can run in - apiGroups: - "" resources: - namespaces verbs: - get +# to define PDBs. Update happens via delete/create - apiGroups: - policy resources: @@ -121,6 +141,7 @@ rules: - create - delete - get +# to create ServiceAccounts in each namespace the operator watches - apiGroups: - "" resources: @@ -128,6 +149,7 @@ rules: verbs: - get - create +# to create role bindings to the operator service account - apiGroups: - "rbac.authorization.k8s.io" resources: @@ -135,18 +157,11 @@ rules: verbs: - get - create -- apiGroups: - - "rbac.authorization.k8s.io" - resources: - - clusterroles - verbs: - - bind - resourceNames: - - zalando-postgres-operator +# to CRUD cron jobs for logical backups - apiGroups: - batch resources: - - cronjobs # enables logical backups + - cronjobs verbs: - create - delete @@ -154,6 +169,7 @@ rules: - list - patch - update + --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/manifests/operatorconfiguration.crd.yaml b/manifests/operatorconfiguration.crd.yaml new file mode 100644 index 000000000..810624bc4 --- /dev/null +++ b/manifests/operatorconfiguration.crd.yaml @@ -0,0 +1,292 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: operatorconfigurations.acid.zalan.do +spec: + group: acid.zalan.do + names: + kind: OperatorConfiguration + listKind: OperatorConfigurationList + plural: operatorconfigurations + singular: operatorconfiguration + shortNames: + - opconfig + scope: Namespaced + subresources: + status: {} + version: v1 + validation: + openAPIV3Schema: + type: object + required: + - kind + - apiVersion + - configuration + properties: + kind: + type: string + enum: + - OperatorConfiguration + apiVersion: + type: string + enum: + - acid.zalan.do/v1 + configuration: + type: object + properties: + docker_image: + type: string + enable_crd_validation: + type: boolean + enable_shm_volume: + type: boolean + etcd_host: + type: string + max_instances: + type: integer + minimum: -1 # -1 = disabled + min_instances: + type: integer + minimum: -1 # -1 = disabled + resync_period: + type: string + repair_period: + type: string + set_memory_request_to_limit: + type: boolean + sidecar_docker_images: + type: object + additionalProperties: + type: string + workers: + type: integer + minimum: 1 + users: + type: object + properties: + replication_username: + type: string + super_username: + type: string + kubernetes: + type: object + properties: + cluster_domain: + type: string + cluster_labels: + type: object + additionalProperties: + type: string + cluster_name_label: + type: string + custom_pod_annotations: + type: object + additionalProperties: + type: string + enable_init_containers: + type: boolean + enable_pod_antiaffinity: + type: boolean + enable_pod_disruption_budget: + type: boolean + enable_sidecars: + type: boolean + infrastructure_roles_secret_name: + type: string + inherited_labels: + type: array + items: + type: string + master_pod_move_timeout: + type: string + node_readiness_label: + type: object + additionalProperties: + type: string + oauth_token_secret_name: + type: string + pdb_name_format: + type: string + pod_antiaffinity_topology_key: + type: string + pod_environment_configmap: + type: string + pod_management_policy: + type: string + enum: + - "ordered_ready" + - "parallel" + pod_priority_class_name: + type: string + pod_role_label: + type: string + pod_service_account_definition: + type: string + pod_service_account_name: + type: string + pod_service_account_role_binding_definition: + type: string + pod_terminate_grace_period: + type: string + secret_name_template: + type: string + spilo_fsgroup: + type: integer + spilo_privileged: + type: boolean + toleration: + type: object + additionalProperties: + type: string + watched_namespace: + type: string + postgres_pod_resources: + type: object + properties: + default_cpu_limit: + type: string + pattern: '^(\d+m|\d+(\.\d{1,3})?)$' + default_cpu_request: + type: string + pattern: '^(\d+m|\d+(\.\d{1,3})?)$' + default_memory_limit: + type: string + pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' + default_memory_request: + type: string + pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' + timeouts: + type: object + properties: + pod_label_wait_timeout: + type: string + pod_deletion_wait_timeout: + type: string + ready_wait_interval: + type: string + ready_wait_timeout: + type: string + resource_check_interval: + type: string + resource_check_timeout: + type: string + load_balancer: + type: object + properties: + custom_service_annotations: + type: object + additionalProperties: + type: string + db_hosted_zone: + type: string + enable_master_load_balancer: + type: boolean + enable_replica_load_balancer: + type: boolean + master_dns_name_format: + type: string + replica_dns_name_format: + type: string + aws_or_gcp: + type: object + properties: + additional_secret_mount: + type: string + additional_secret_mount_path: + type: string + aws_region: + type: string + kube_iam_role: + type: string + log_s3_bucket: + type: string + wal_s3_bucket: + type: string + logical_backup: + type: object + properties: + logical_backup_docker_image: + type: string + logical_backup_s3_access_key_id: + type: string + logical_backup_s3_bucket: + type: string + logical_backup_s3_endpoint: + type: string + logical_backup_s3_secret_access_key: + type: string + logical_backup_s3_sse: + type: string + logical_backup_schedule: + type: string + pattern: '^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$' + debug: + type: object + properties: + debug_logging: + type: boolean + enable_database_access: + type: boolean + teams_api: + type: object + properties: + enable_admin_role_for_users: + type: boolean + enable_team_superuser: + type: boolean + enable_teams_api: + type: boolean + pam_configuration: + type: string + pam_role_name: + type: string + postgres_superuser_teams: + type: array + items: + type: string + protected_role_names: + type: array + items: + type: string + team_admin_role: + type: string + team_api_role_configuration: + type: object + additionalProperties: + type: string + teams_api_url: + type: string + logging_rest_api: + type: object + properties: + api_port: + type: integer + cluster_history_entries: + type: integer + ring_log_lines: + type: integer + scalyr: + type: object + properties: + scalyr_api_key: + type: string + scalyr_cpu_limit: + type: string + pattern: '^(\d+m|\d+(\.\d{1,3})?)$' + scalyr_cpu_request: + type: string + pattern: '^(\d+m|\d+(\.\d{1,3})?)$' + scalyr_image: + type: string + scalyr_memory_limit: + type: string + pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' + scalyr_memory_request: + type: string + pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' + scalyr_server_url: + type: string + status: + type: object + additionalProperties: + type: string diff --git a/manifests/postgres-operator.yaml b/manifests/postgres-operator.yaml index 7b74b80a0..498796b61 100644 --- a/manifests/postgres-operator.yaml +++ b/manifests/postgres-operator.yaml @@ -15,7 +15,7 @@ spec: serviceAccountName: zalando-postgres-operator containers: - name: postgres-operator - image: registry.opensource.zalan.do/acid/postgres-operator:v1.2.0 + image: registry.opensource.zalan.do/acid/postgres-operator:v1.3.0 imagePullPolicy: IfNotPresent resources: requests: diff --git a/manifests/postgresql-operator-default-configuration.yaml b/manifests/postgresql-operator-default-configuration.yaml index 3c79828d5..5e10ff66e 100644 --- a/manifests/postgresql-operator-default-configuration.yaml +++ b/manifests/postgresql-operator-default-configuration.yaml @@ -3,8 +3,9 @@ kind: OperatorConfiguration metadata: name: postgresql-operator-default-configuration configuration: + # enable_crd_validation: true etcd_host: "" - docker_image: registry.opensource.zalan.do/acid/spilo-11:1.5-p9 + docker_image: registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p16 # enable_shm_volume: true max_instances: -1 min_instances: -1 @@ -20,28 +21,38 @@ configuration: kubernetes: cluster_domain: cluster.local cluster_labels: - application: spilo + application: spilo cluster_name_label: cluster-name + # custom_pod_annotations: + # keya: valuea + # keyb: valueb + enable_init_containers: true enable_pod_antiaffinity: false enable_pod_disruption_budget: true - # infrastructure_roles_secret_name: "" + enable_sidecars: true + # infrastructure_roles_secret_name: "postgresql-infrastructure-roles" # inherited_labels: # - application - # - app - # node_readiness_label: "" + # - environment + master_pod_move_timeout: 20m + # node_readiness_label: + # status: ready oauth_token_secret_name: postgresql-operator pdb_name_format: "postgres-{cluster}-pdb" pod_antiaffinity_topology_key: "kubernetes.io/hostname" # pod_environment_configmap: "" pod_management_policy: "ordered_ready" + # pod_priority_class_name: "" pod_role_label: spilo-role - pod_service_account_name: operator + # pod_service_account_definition: "" + pod_service_account_name: zalando-postgres-operator + # pod_service_account_role_binding_definition: "" pod_terminate_grace_period: 5m secret_name_template: "{username}.{cluster}.credentials.{tprkind}.{tprgroup}" # spilo_fsgroup: 103 spilo_privileged: false # toleration: {} - # watched_namespace:"" + # watched_namespace: "" postgres_pod_resources: default_cpu_limit: "3" default_cpu_request: 100m @@ -71,9 +82,13 @@ configuration: # log_s3_bucket: "" # wal_s3_bucket: "" logical_backup: - logical_backup_schedule: "30 00 * * *" logical_backup_docker_image: "registry.opensource.zalan.do/acid/logical-backup" + # logical_backup_s3_access_key_id: "" logical_backup_s3_bucket: "my-bucket-url" + # logical_backup_s3_endpoint: "" + # logical_backup_s3_secret_access_key: "" + logical_backup_s3_sse: "AES256" + logical_backup_schedule: "30 00 * * *" debug: debug_logging: true enable_database_access: true @@ -83,9 +98,10 @@ configuration: enable_teams_api: false # pam_configuration: "" pam_role_name: zalandos - # postgres_superuser_teams: "postgres_superusers" + # postgres_superuser_teams: + # - postgres_superusers protected_role_names: - - admin + - admin team_admin_role: admin team_api_role_configuration: log_statement: all diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml new file mode 100644 index 000000000..3b0f652ea --- /dev/null +++ b/manifests/postgresql.crd.yaml @@ -0,0 +1,331 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: postgresqls.acid.zalan.do +spec: + group: acid.zalan.do + names: + kind: postgresql + listKind: postgresqlList + plural: postgresqls + singular: postgresql + shortNames: + - pg + scope: Namespaced + subresources: + status: {} + version: v1 + validation: + openAPIV3Schema: + type: object + required: + - kind + - apiVersion + - spec + properties: + kind: + type: string + enum: + - postgresql + apiVersion: + type: string + enum: + - acid.zalan.do/v1 + spec: + type: object + required: + - numberOfInstances + - teamId + - postgresql + properties: + allowedSourceRanges: + type: array + nullable: true + items: + type: string + pattern: '^(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])$' + clone: + type: object + required: + - cluster + properties: + cluster: + type: string + s3_endpoint: + type: string + s3_access_key_id: + type: string + s3_secret_access_key: + type: string + s3_force_path_style: + type: string + s3_wal_path: + type: string + timestamp: + type: string + pattern: '^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([+-]([01][0-9]|2[0-3]):[0-5][0-9]))$' + # The regexp matches the date-time format (RFC 3339 Section 5.6) that specifies a timezone as an offset relative to UTC + # Example: 1996-12-19T16:39:57-08:00 + # Note: this field requires a timezone + uid: + format: uuid + type: string + databases: + type: object + additionalProperties: + type: string + # Note: usernames specified here as database owners must be declared in the users key of the spec key. + dockerImage: + type: string + enableLogicalBackup: + type: boolean + enableMasterLoadBalancer: + type: boolean + enableReplicaLoadBalancer: + type: boolean + enableShmVolume: + type: boolean + init_containers: # deprecated + type: array + nullable: true + items: + type: object + additionalProperties: true + initContainers: + type: array + nullable: true + items: + type: object + additionalProperties: true + logicalBackupSchedule: + type: string + pattern: '^(\d+|\*)(/\d+)?(\s+(\d+|\*)(/\d+)?){4}$' + maintenanceWindows: + type: array + items: + type: string + pattern: '^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ *$' + numberOfInstances: + type: integer + minimum: 0 + patroni: + type: object + properties: + initdb: + type: object + additionalProperties: + type: string + pg_hba: + type: array + items: + type: string + slots: + type: object + additionalProperties: + type: object + additionalProperties: + type: string + ttl: + type: integer + loop_wait: + type: integer + retry_timeout: + type: integer + maximum_lag_on_failover: + type: integer + podAnnotations: + type: object + additionalProperties: + type: string + pod_priority_class_name: # deprecated + type: string + podPriorityClassName: + type: string + postgresql: + type: object + required: + - version + properties: + version: + type: string + enum: + - "9.3" + - "9.4" + - "9.5" + - "9.6" + - "10" + - "11" + - "12" + parameters: + type: object + additionalProperties: + type: string + replicaLoadBalancer: # deprecated + type: boolean + resources: + type: object + required: + - requests + - limits + properties: + limits: + type: object + required: + - cpu + - memory + properties: + cpu: + type: string + # Decimal natural followed by m, or decimal natural followed by + # dot followed by up to three decimal digits. + # + # This is because the Kubernetes CPU resource has millis as the + # maximum precision. The actual values are checked in code + # because the regular expression would be huge and horrible and + # not very helpful in validation error messages; this one checks + # only the format of the given number. + # + # https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu + pattern: '^(\d+m|\d+(\.\d{1,3})?)$' + # Note: the value specified here must not be zero or be lower + # than the corresponding request. + memory: + type: string + # You can express memory as a plain integer or as a fixed-point + # integer using one of these suffixes: E, P, T, G, M, k. You can + # also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki + # + # https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory + pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' + # Note: the value specified here must not be zero or be lower + # than the corresponding request. + requests: + type: object + required: + - cpu + - memory + properties: + cpu: + type: string + # Decimal natural followed by m, or decimal natural followed by + # dot followed by up to three decimal digits. + # + # This is because the Kubernetes CPU resource has millis as the + # maximum precision. The actual values are checked in code + # because the regular expression would be huge and horrible and + # not very helpful in validation error messages; this one checks + # only the format of the given number. + # + # https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-cpu + pattern: '^(\d+m|\d+(\.\d{1,3})?)$' + # Note: the value specified here must not be zero or be higher + # than the corresponding limit. + memory: + type: string + # You can express memory as a plain integer or as a fixed-point + # integer using one of these suffixes: E, P, T, G, M, k. You can + # also use the power-of-two equivalents: Ei, Pi, Ti, Gi, Mi, Ki + # + # https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/#meaning-of-memory + pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' + # Note: the value specified here must not be zero or be higher + # than the corresponding limit. + sidecars: + type: array + nullable: true + items: + type: object + additionalProperties: true + spiloFSGroup: + type: integer + standby: + type: object + required: + - s3_wal_path + properties: + s3_wal_path: + type: string + teamId: + type: string + tolerations: + type: array + items: + type: object + required: + - key + - operator + - effect + properties: + key: + type: string + operator: + type: string + enum: + - Equal + - Exists + value: + type: string + effect: + type: string + enum: + - NoExecute + - NoSchedule + - PreferNoSchedule + tolerationSeconds: + type: integer + useLoadBalancer: # deprecated + type: boolean + users: + type: object + additionalProperties: + type: array + nullable: true + description: "Role flags specified here must not contradict each other" + items: + type: string + enum: + - bypassrls + - BYPASSRLS + - nobypassrls + - NOBYPASSRLS + - createdb + - CREATEDB + - nocreatedb + - NOCREATEDB + - createrole + - CREATEROLE + - nocreaterole + - NOCREATEROLE + - inherit + - INHERIT + - noinherit + - NOINHERIT + - login + - LOGIN + - nologin + - NOLOGIN + - replication + - REPLICATION + - noreplication + - NOREPLICATION + - superuser + - SUPERUSER + - nosuperuser + - NOSUPERUSER + volume: + type: object + required: + - size + properties: + size: + type: string + pattern: '^(\d+(e\d+)?|\d+(\.\d+)?(e\d+)?[EPTGMK]i?)$' + # Note: the value specified here must not be zero. + storageClass: + type: string + subPath: + type: string + status: + type: object + additionalProperties: + type: string diff --git a/manifests/standby-manifest.yaml b/manifests/standby-manifest.yaml index 49f2b1a1f..2b621bd10 100644 --- a/manifests/standby-manifest.yaml +++ b/manifests/standby-manifest.yaml @@ -4,16 +4,12 @@ metadata: name: acid-standby-cluster namespace: default spec: - teamId: "ACID" + teamId: "acid" volume: size: 1Gi numberOfInstances: 1 postgresql: - version: "10" + version: "11" # Make this a standby cluster and provide the s3 bucket path of source cluster for continuous streaming. standby: s3_wal_path: "s3://path/to/bucket/containing/wal/of/source/cluster/" - - maintenanceWindows: - - 01:00-06:00 #UTC - - Sat:00:00-04:00 diff --git a/pkg/apis/acid.zalan.do/v1/crds.go b/pkg/apis/acid.zalan.do/v1/crds.go index 50833db26..20fa37138 100644 --- a/pkg/apis/acid.zalan.do/v1/crds.go +++ b/pkg/apis/acid.zalan.do/v1/crds.go @@ -1,7 +1,7 @@ package v1 import ( - "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do" + acidzalando "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do" apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -103,7 +103,936 @@ var OperatorConfigCRDResourceColumns = []apiextv1beta1.CustomResourceColumnDefin }, } -func buildCRD(name, kind, plural, short string, columns []apiextv1beta1.CustomResourceColumnDefinition) *apiextv1beta1.CustomResourceDefinition { +var min0 = 0.0 +var min1 = 1.0 +var minDisable = -1.0 + +// PostgresCRDResourceValidation to check applied manifest parameters +var PostgresCRDResourceValidation = apiextv1beta1.CustomResourceValidation{ + OpenAPIV3Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "object", + Required: []string{"kind", "apiVersion", "spec"}, + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "kind": { + Type: "string", + Enum: []apiextv1beta1.JSON{ + { + Raw: []byte(`"postgresql"`), + }, + }, + }, + "apiVersion": { + Type: "string", + Enum: []apiextv1beta1.JSON{ + { + Raw: []byte(`"acid.zalan.do/v1"`), + }, + }, + }, + "spec": { + Type: "object", + Required: []string{"numberOfInstances", "teamId", "postgresql"}, + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "allowedSourceRanges": { + Type: "array", + Nullable: true, + Items: &apiextv1beta1.JSONSchemaPropsOrArray{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + Pattern: "^(\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\\.(\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\\.(\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\\.(\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])\\/(\\d|[1-2]\\d|3[0-2])$", + }, + }, + }, + "clone": { + Type: "object", + Required: []string{"cluster"}, + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "cluster": { + Type: "string", + }, + "s3_endpoint": { + Type: "string", + }, + "s3_access_key_id": { + Type: "string", + }, + "s3_secret_access_key": { + Type: "string", + }, + "s3_force_path_style": { + Type: "string", + }, + "s3_wal_path": { + Type: "string", + }, + "timestamp": { + Type: "string", + Description: "Date-time format that specifies a timezone as an offset relative to UTC e.g. 1996-12-19T16:39:57-08:00", + Pattern: "^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\\.[0-9]+)?(([Zz])|([+-]([01][0-9]|2[0-3]):[0-5][0-9]))$", + }, + "uid": { + Type: "string", + Format: "uuid", + }, + }, + }, + "databases": { + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + Description: "User names specified here as database owners must be declared in the users key of the spec key", + }, + }, + }, + "dockerImage": { + Type: "string", + }, + "enableLogicalBackup": { + Type: "boolean", + }, + "enableMasterLoadBalancer": { + Type: "boolean", + }, + "enableReplicaLoadBalancer": { + Type: "boolean", + }, + "enableShmVolume": { + Type: "boolean", + }, + "init_containers": { + Type: "array", + Description: "Deprecated", + Items: &apiextv1beta1.JSONSchemaPropsOrArray{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Allows: true, + }, + }, + }, + }, + "initContainers": { + Type: "array", + Items: &apiextv1beta1.JSONSchemaPropsOrArray{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Allows: true, + }, + }, + }, + }, + "logicalBackupSchedule": { + Type: "string", + Pattern: "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$", + }, + "maintenanceWindows": { + Type: "array", + Items: &apiextv1beta1.JSONSchemaPropsOrArray{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + Pattern: "^\\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\\d):([0-5]?\\d)|(2[0-3]|[01]?\\d):([0-5]?\\d))-((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\\d):([0-5]?\\d)|(2[0-3]|[01]?\\d):([0-5]?\\d))\\ *$", + }, + }, + }, + "numberOfInstances": { + Type: "integer", + Minimum: &min0, + }, + "patroni": { + Type: "object", + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "initdb": { + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + "pg_hba": { + Type: "array", + Items: &apiextv1beta1.JSONSchemaPropsOrArray{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + "slots": { + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + }, + }, + "ttl": { + Type: "integer", + }, + "loop_wait": { + Type: "integer", + }, + "retry_timeout": { + Type: "integer", + }, + "maximum_lag_on_failover": { + Type: "integer", + }, + }, + }, + "podAnnotations": { + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + "pod_priority_class_name": { + Type: "string", + Description: "Deprecated", + }, + "podPriorityClassName": { + Type: "string", + }, + "postgresql": { + Type: "object", + Required: []string{"version"}, + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "version": { + Type: "string", + Enum: []apiextv1beta1.JSON{ + { + Raw: []byte(`"9.3"`), + }, + { + Raw: []byte(`"9.4"`), + }, + { + Raw: []byte(`"9.5"`), + }, + { + Raw: []byte(`"9.6"`), + }, + { + Raw: []byte(`"10"`), + }, + { + Raw: []byte(`"11"`), + }, + { + Raw: []byte(`"12"`), + }, + }, + }, + "parameters": { + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + }, + }, + "replicaLoadBalancer": { + Type: "boolean", + Description: "Deprecated", + }, + "resources": { + Type: "object", + Required: []string{"requests", "limits"}, + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "limits": { + Type: "object", + Required: []string{"cpu", "memory"}, + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "cpu": { + Type: "string", + Description: "Decimal natural followed by m, or decimal natural followed by dot followed by up to three decimal digits (precision used by Kubernetes). Must be greater than 0", + Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$", + }, + "memory": { + Type: "string", + Description: "Plain integer or fixed-point integer using one of these suffixes: E, P, T, G, M, k (with or without a tailing i). Must be greater than 0", + Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$", + }, + }, + }, + "requests": { + Type: "object", + Required: []string{"cpu", "memory"}, + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "cpu": { + Type: "string", + Description: "Decimal natural followed by m, or decimal natural followed by dot followed by up to three decimal digits (precision used by Kubernetes). Must be greater than 0", + Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$", + }, + "memory": { + Type: "string", + Description: "Plain integer or fixed-point integer using one of these suffixes: E, P, T, G, M, k (with or without a tailing i). Must be greater than 0", + Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$", + }, + }, + }, + }, + }, + "sidecars": { + Type: "array", + Items: &apiextv1beta1.JSONSchemaPropsOrArray{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Allows: true, + }, + }, + }, + }, + "spiloFSGroup": { + Type: "integer", + }, + "standby": { + Type: "object", + Required: []string{"s3_wal_path"}, + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "s3_wal_path": { + Type: "string", + }, + }, + }, + "teamId": { + Type: "string", + }, + "tolerations": { + Type: "array", + Items: &apiextv1beta1.JSONSchemaPropsOrArray{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "object", + Required: []string{"key", "operator", "effect"}, + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "key": { + Type: "string", + }, + "operator": { + Type: "string", + Enum: []apiextv1beta1.JSON{ + { + Raw: []byte(`"Equal"`), + }, + { + Raw: []byte(`"Exists"`), + }, + }, + }, + "value": { + Type: "string", + }, + "effect": { + Type: "string", + Enum: []apiextv1beta1.JSON{ + { + Raw: []byte(`"NoExecute"`), + }, + { + Raw: []byte(`"NoSchedule"`), + }, + { + Raw: []byte(`"PreferNoSchedule"`), + }, + }, + }, + "tolerationSeconds": { + Type: "integer", + }, + }, + }, + }, + }, + "useLoadBalancer": { + Type: "boolean", + Description: "Deprecated", + }, + "users": { + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "array", + Description: "Role flags specified here must not contradict each other", + Nullable: true, + Items: &apiextv1beta1.JSONSchemaPropsOrArray{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + Enum: []apiextv1beta1.JSON{ + { + Raw: []byte(`"bypassrls"`), + }, + { + Raw: []byte(`"BYPASSRLS"`), + }, + { + Raw: []byte(`"nobypassrls"`), + }, + { + Raw: []byte(`"NOBYPASSRLS"`), + }, + { + Raw: []byte(`"createdb"`), + }, + { + Raw: []byte(`"CREATEDB"`), + }, + { + Raw: []byte(`"nocreatedb"`), + }, + { + Raw: []byte(`"NOCREATEDB"`), + }, + { + Raw: []byte(`"createrole"`), + }, + { + Raw: []byte(`"CREATEROLE"`), + }, + { + Raw: []byte(`"nocreaterole"`), + }, + { + Raw: []byte(`"NOCREATEROLE"`), + }, + { + Raw: []byte(`"inherit"`), + }, + { + Raw: []byte(`"INHERIT"`), + }, + { + Raw: []byte(`"noinherit"`), + }, + { + Raw: []byte(`"NOINHERIT"`), + }, + { + Raw: []byte(`"login"`), + }, + { + Raw: []byte(`"LOGIN"`), + }, + { + Raw: []byte(`"nologin"`), + }, + { + Raw: []byte(`"NOLOGIN"`), + }, + { + Raw: []byte(`"replication"`), + }, + { + Raw: []byte(`"REPLICATION"`), + }, + { + Raw: []byte(`"noreplication"`), + }, + { + Raw: []byte(`"NOREPLICATION"`), + }, + { + Raw: []byte(`"superuser"`), + }, + { + Raw: []byte(`"SUPERUSER"`), + }, + { + Raw: []byte(`"nosuperuser"`), + }, + { + Raw: []byte(`"NOSUPERUSER"`), + }, + }, + }, + }, + }, + }, + }, + "volume": { + Type: "object", + Required: []string{"size"}, + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "size": { + Type: "string", + Description: "Value must not be zero", + Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$", + }, + "storageClass": { + Type: "string", + }, + "subPath": { + Type: "string", + }, + }, + }, + }, + }, + "status": { + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + }, + }, +} + +// OperatorConfigCRDResourceValidation to check applied manifest parameters +var OperatorConfigCRDResourceValidation = apiextv1beta1.CustomResourceValidation{ + OpenAPIV3Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "object", + Required: []string{"kind", "apiVersion", "configuration"}, + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "kind": { + Type: "string", + Enum: []apiextv1beta1.JSON{ + { + Raw: []byte(`"OperatorConfiguration"`), + }, + }, + }, + "apiVersion": { + Type: "string", + Enum: []apiextv1beta1.JSON{ + { + Raw: []byte(`"acid.zalan.do/v1"`), + }, + }, + }, + "configuration": { + Type: "object", + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "docker_image": { + Type: "string", + }, + "enable_crd_validation": { + Type: "boolean", + }, + "enable_shm_volume": { + Type: "boolean", + }, + "etcd_host": { + Type: "string", + }, + "max_instances": { + Type: "integer", + Description: "-1 = disabled", + Minimum: &minDisable, + }, + "min_instances": { + Type: "integer", + Description: "-1 = disabled", + Minimum: &minDisable, + }, + "resync_period": { + Type: "string", + }, + "repair_period": { + Type: "string", + }, + "set_memory_request_to_limit": { + Type: "boolean", + }, + "sidecar_docker_images": { + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + "workers": { + Type: "integer", + Minimum: &min1, + }, + "users": { + Type: "object", + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "replication_username": { + Type: "string", + }, + "super_username": { + Type: "string", + }, + }, + }, + "kubernetes": { + Type: "object", + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "cluster_domain": { + Type: "string", + }, + "cluster_labels": { + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + "cluster_name_label": { + Type: "string", + }, + "custom_pod_annotations": { + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + "enable_init_containers": { + Type: "boolean", + }, + "enable_pod_antiaffinity": { + Type: "boolean", + }, + "enable_pod_disruption_budget": { + Type: "boolean", + }, + "enable_sidecars": { + Type: "boolean", + }, + "infrastructure_roles_secret_name": { + Type: "string", + }, + "inherited_labels": { + Type: "array", + Items: &apiextv1beta1.JSONSchemaPropsOrArray{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + "master_pod_move_timeout": { + Type: "string", + }, + "node_readiness_label": { + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + "oauth_token_secret_name": { + Type: "string", + }, + "pdb_name_format": { + Type: "string", + }, + "pod_antiaffinity_topology_key": { + Type: "string", + }, + "pod_environment_configmap": { + Type: "string", + }, + "pod_management_policy": { + Type: "string", + Enum: []apiextv1beta1.JSON{ + { + Raw: []byte(`"ordered_ready"`), + }, + { + Raw: []byte(`"parallel"`), + }, + }, + }, + "pod_priority_class_name": { + Type: "string", + }, + "pod_role_label": { + Type: "string", + }, + "pod_service_account_definition": { + Type: "string", + }, + "pod_service_account_name": { + Type: "string", + }, + "pod_service_account_role_binding_definition": { + Type: "string", + }, + "pod_terminate_grace_period": { + Type: "string", + }, + "secret_name_template": { + Type: "string", + }, + "spilo_fsgroup": { + Type: "integer", + }, + "spilo_privileged": { + Type: "boolean", + }, + "toleration": { + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + "watched_namespace": { + Type: "string", + }, + }, + }, + "postgres_pod_resources": { + Type: "object", + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "default_cpu_limit": { + Type: "string", + Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$", + }, + "default_cpu_request": { + Type: "string", + Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$", + }, + "default_memory_limit": { + Type: "string", + Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$", + }, + "default_memory_request": { + Type: "string", + Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$", + }, + }, + }, + "timeouts": { + Type: "object", + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "pod_label_wait_timeout": { + Type: "string", + }, + "pod_deletion_wait_timeout": { + Type: "string", + }, + "ready_wait_interval": { + Type: "string", + }, + "ready_wait_timeout": { + Type: "string", + }, + "resource_check_interval": { + Type: "string", + }, + "resource_check_timeout": { + Type: "string", + }, + }, + }, + "load_balancer": { + Type: "object", + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "custom_service_annotations": { + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + "db_hosted_zone": { + Type: "string", + }, + "enable_master_load_balancer": { + Type: "boolean", + }, + "enable_replica_load_balancer": { + Type: "boolean", + }, + "master_dns_name_format": { + Type: "string", + }, + "replica_dns_name_format": { + Type: "string", + }, + }, + }, + "aws_or_gcp": { + Type: "object", + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "additional_secret_mount": { + Type: "string", + }, + "additional_secret_mount_path": { + Type: "string", + }, + "aws_region": { + Type: "string", + }, + "kube_iam_role": { + Type: "string", + }, + "log_s3_bucket": { + Type: "string", + }, + "wal_s3_bucket": { + Type: "string", + }, + }, + }, + "logical_backup": { + Type: "object", + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "logical_backup_docker_image": { + Type: "string", + }, + "logical_backup_s3_access_key_id": { + Type: "string", + }, + "logical_backup_s3_bucket": { + Type: "string", + }, + "logical_backup_s3_endpoint": { + Type: "string", + }, + "logical_backup_s3_secret_access_key": { + Type: "string", + }, + "logical_backup_s3_sse": { + Type: "string", + }, + "logical_backup_schedule": { + Type: "string", + Pattern: "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$", + }, + }, + }, + "debug": { + Type: "object", + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "debug_logging": { + Type: "boolean", + }, + "enable_database_access": { + Type: "boolean", + }, + }, + }, + "teams_api": { + Type: "object", + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "enable_admin_role_for_users": { + Type: "boolean", + }, + "enable_team_superuser": { + Type: "boolean", + }, + "enable_teams_api": { + Type: "boolean", + }, + "pam_configuration": { + Type: "string", + }, + "pam_role_name": { + Type: "string", + }, + "postgres_superuser_teams": { + Type: "array", + Items: &apiextv1beta1.JSONSchemaPropsOrArray{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + "protected_role_names": { + Type: "array", + Items: &apiextv1beta1.JSONSchemaPropsOrArray{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + "team_admin_role": { + Type: "string", + }, + "team_api_role_configuration": { + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + "teams_api_url": { + Type: "string", + }, + }, + }, + "logging_rest_api": { + Type: "object", + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "api_port": { + Type: "integer", + }, + "cluster_history_entries": { + Type: "integer", + }, + "ring_log_lines": { + Type: "integer", + }, + }, + }, + "scalyr": { + Type: "object", + Properties: map[string]apiextv1beta1.JSONSchemaProps{ + "scalyr_api_key": { + Type: "string", + }, + "scalyr_cpu_limit": { + Type: "string", + Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$", + }, + "scalyr_cpu_request": { + Type: "string", + Pattern: "^(\\d+m|\\d+(\\.\\d{1,3})?)$", + }, + "scalyr_image": { + Type: "string", + }, + "scalyr_memory_limit": { + Type: "string", + Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$", + }, + "scalyr_memory_request": { + Type: "string", + Pattern: "^(\\d+(e\\d+)?|\\d+(\\.\\d+)?(e\\d+)?[EPTGMK]i?)$", + }, + "scalyr_server_url": { + Type: "string", + }, + }, + }, + }, + }, + "status": { + Type: "object", + AdditionalProperties: &apiextv1beta1.JSONSchemaPropsOrBool{ + Schema: &apiextv1beta1.JSONSchemaProps{ + Type: "string", + }, + }, + }, + }, + }, +} + +func buildCRD(name, kind, plural, short string, columns []apiextv1beta1.CustomResourceColumnDefinition, validation apiextv1beta1.CustomResourceValidation) *apiextv1beta1.CustomResourceDefinition { return &apiextv1beta1.CustomResourceDefinition{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -121,24 +1050,39 @@ func buildCRD(name, kind, plural, short string, columns []apiextv1beta1.CustomRe Status: &apiextv1beta1.CustomResourceSubresourceStatus{}, }, AdditionalPrinterColumns: columns, + Validation: &validation, }, } } // PostgresCRD returns CustomResourceDefinition built from PostgresCRDResource -func PostgresCRD() *apiextv1beta1.CustomResourceDefinition { +func PostgresCRD(enableValidation *bool) *apiextv1beta1.CustomResourceDefinition { + postgresCRDvalidation := apiextv1beta1.CustomResourceValidation{} + + if enableValidation != nil && *enableValidation { + postgresCRDvalidation = PostgresCRDResourceValidation + } + return buildCRD(PostgresCRDResouceName, PostgresCRDResourceKind, PostgresCRDResourcePlural, PostgresCRDResourceShort, - PostgresCRDResourceColumns) + PostgresCRDResourceColumns, + postgresCRDvalidation) } // ConfigurationCRD returns CustomResourceDefinition built from OperatorConfigCRDResource -func ConfigurationCRD() *apiextv1beta1.CustomResourceDefinition { +func ConfigurationCRD(enableValidation *bool) *apiextv1beta1.CustomResourceDefinition { + opconfigCRDvalidation := apiextv1beta1.CustomResourceValidation{} + + if enableValidation != nil && *enableValidation { + opconfigCRDvalidation = OperatorConfigCRDResourceValidation + } + return buildCRD(OperatorConfigCRDResourceName, OperatorConfigCRDResouceKind, OperatorConfigCRDResourcePlural, OperatorConfigCRDResourceShort, - OperatorConfigCRDResourceColumns) + OperatorConfigCRDResourceColumns, + opconfigCRDvalidation) } diff --git a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go index 6f1c8d59c..948c7cbbf 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -45,11 +45,13 @@ type KubernetesMetaConfiguration struct { PodServiceAccountDefinition string `json:"pod_service_account_definition,omitempty"` PodServiceAccountRoleBindingDefinition string `json:"pod_service_account_role_binding_definition,omitempty"` PodTerminateGracePeriod Duration `json:"pod_terminate_grace_period,omitempty"` - SpiloPrivileged bool `json:"spilo_privileged,omitemty"` + SpiloPrivileged bool `json:"spilo_privileged,omitempty"` SpiloFSGroup *int64 `json:"spilo_fsgroup,omitempty"` WatchedNamespace string `json:"watched_namespace,omitempty"` PDBNameFormat config.StringTemplate `json:"pdb_name_format,omitempty"` EnablePodDisruptionBudget *bool `json:"enable_pod_disruption_budget,omitempty"` + EnableInitContainers *bool `json:"enable_init_containers,omitempty"` + EnableSidecars *bool `json:"enable_sidecars,omitempty"` SecretNameTemplate config.StringTemplate `json:"secret_name_template,omitempty"` ClusterDomain string `json:"cluster_domain"` OAuthTokenSecretName spec.NamespacedName `json:"oauth_token_secret_name,omitempty"` @@ -59,6 +61,7 @@ type KubernetesMetaConfiguration struct { InheritedLabels []string `json:"inherited_labels,omitempty"` ClusterNameLabel string `json:"cluster_name_label,omitempty"` NodeReadinessLabel map[string]string `json:"node_readiness_label,omitempty"` + CustomPodAnnotations map[string]string `json:"custom_pod_annotations,omitempty"` // TODO: use a proper toleration structure? PodToleration map[string]string `json:"toleration,omitempty"` // TODO: use namespacedname @@ -115,7 +118,7 @@ type OperatorDebugConfiguration struct { EnableDBAccess bool `json:"enable_database_access,omitempty"` } -// TeamsAPIConfiguration defines the configration of TeamsAPI +// TeamsAPIConfiguration defines the configuration of TeamsAPI type TeamsAPIConfiguration struct { EnableTeamsAPI bool `json:"enable_teams_api,omitempty"` TeamsAPIUrl string `json:"teams_api_url,omitempty"` @@ -147,8 +150,20 @@ type ScalyrConfiguration struct { ScalyrMemoryLimit string `json:"scalyr_memory_limit,omitempty"` } +// OperatorLogicalBackupConfiguration defines configuration for logical backup +type OperatorLogicalBackupConfiguration struct { + Schedule string `json:"logical_backup_schedule,omitempty"` + DockerImage string `json:"logical_backup_docker_image,omitempty"` + S3Bucket string `json:"logical_backup_s3_bucket,omitempty"` + S3Endpoint string `json:"logical_backup_s3_endpoint,omitempty"` + S3AccessKeyID string `json:"logical_backup_s3_access_key_id,omitempty"` + S3SecretAccessKey string `json:"logical_backup_s3_secret_access_key,omitempty"` + S3SSE string `json:"logical_backup_s3_sse,omitempty"` +} + // OperatorConfigurationData defines the operation config type OperatorConfigurationData struct { + EnableCRDValidation *bool `json:"enable_crd_validation,omitempty"` EtcdHost string `json:"etcd_host,omitempty"` DockerImage string `json:"docker_image,omitempty"` Workers uint32 `json:"workers,omitempty"` @@ -172,19 +187,5 @@ type OperatorConfigurationData struct { LogicalBackup OperatorLogicalBackupConfiguration `json:"logical_backup"` } -// OperatorConfigurationUsers defines configration for super user -type OperatorConfigurationUsers struct { - SuperUserName string `json:"superuser_name,omitempty"` - Replication string `json:"replication_user_name,omitempty"` - ProtectedRoles []string `json:"protected_roles,omitempty"` - TeamAPIRoleConfiguration map[string]string `json:"team_api_role_configuration,omitempty"` -} - //Duration shortens this frequently used name type Duration time.Duration - -type OperatorLogicalBackupConfiguration struct { - Schedule string `json:"logical_backup_schedule,omitempty"` - DockerImage string `json:"logical_backup_docker_image,omitempty"` - S3Bucket string `json:"logical_backup_s3_bucket,omitempty"` -} diff --git a/pkg/apis/acid.zalan.do/v1/postgresql_type.go b/pkg/apis/acid.zalan.do/v1/postgresql_type.go index 72e40e122..515a73ff0 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql_type.go +++ b/pkg/apis/acid.zalan.do/v1/postgresql_type.go @@ -59,6 +59,7 @@ type PostgresSpec struct { EnableLogicalBackup bool `json:"enableLogicalBackup,omitempty"` LogicalBackupSchedule string `json:"logicalBackupSchedule,omitempty"` StandbyCluster *StandbyDescription `json:"standby"` + PodAnnotations map[string]string `json:"podAnnotations"` // deprecated json tags InitContainersOld []v1.Container `json:"init_containers,omitempty"` diff --git a/pkg/apis/acid.zalan.do/v1/util_test.go b/pkg/apis/acid.zalan.do/v1/util_test.go index 6c1b63ece..fc068b322 100644 --- a/pkg/apis/acid.zalan.do/v1/util_test.go +++ b/pkg/apis/acid.zalan.do/v1/util_test.go @@ -180,7 +180,7 @@ var unmarshalCluster = []struct { "name": "acid-testcluster1" }, "spec": { - "teamId": "ACID", + "teamId": "acid", "pod_priority_class_name": "spilo-pod-priority", "volume": { "size": "5Gi", @@ -290,7 +290,7 @@ var unmarshalCluster = []struct { ResourceLimits: ResourceDescription{CPU: "300m", Memory: "3000Mi"}, }, - TeamID: "ACID", + TeamID: "acid", AllowedSourceRanges: []string{"127.0.0.1/32"}, NumberOfInstances: 2, Users: map[string]UserFlags{"zalando": {"superuser", "createdb"}}, @@ -319,7 +319,7 @@ var unmarshalCluster = []struct { }, Error: "", }, - marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"9.6","parameters":{"log_statement":"all","max_connections":"10","shared_buffers":"32MB"}},"pod_priority_class_name":"spilo-pod-priority","volume":{"size":"5Gi","storageClass":"SSD", "subPath": "subdir"},"enableShmVolume":false,"patroni":{"initdb":{"data-checksums":"true","encoding":"UTF8","locale":"en_US.UTF-8"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"],"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}}},"resources":{"requests":{"cpu":"10m","memory":"50Mi"},"limits":{"cpu":"300m","memory":"3000Mi"}},"teamId":"ACID","allowedSourceRanges":["127.0.0.1/32"],"numberOfInstances":2,"users":{"zalando":["superuser","createdb"]},"maintenanceWindows":["Mon:01:00-06:00","Sat:00:00-04:00","05:00-05:15"],"clone":{"cluster":"acid-batman"}},"status":{"PostgresClusterStatus":""}}`), + marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"9.6","parameters":{"log_statement":"all","max_connections":"10","shared_buffers":"32MB"}},"pod_priority_class_name":"spilo-pod-priority","volume":{"size":"5Gi","storageClass":"SSD", "subPath": "subdir"},"enableShmVolume":false,"patroni":{"initdb":{"data-checksums":"true","encoding":"UTF8","locale":"en_US.UTF-8"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"],"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}}},"resources":{"requests":{"cpu":"10m","memory":"50Mi"},"limits":{"cpu":"300m","memory":"3000Mi"}},"teamId":"acid","allowedSourceRanges":["127.0.0.1/32"],"numberOfInstances":2,"users":{"zalando":["superuser","createdb"]},"maintenanceWindows":["Mon:01:00-06:00","Sat:00:00-04:00","05:00-05:15"],"clone":{"cluster":"acid-batman"}},"status":{"PostgresClusterStatus":""}}`), err: nil}, // example with teamId set in input { @@ -437,6 +437,16 @@ var postgresqlList = []struct { PostgresqlList{}, errors.New("unexpected end of JSON input")}} +var annotations = []struct { + in []byte + annotations map[string]string + err error +}{{ + in: []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1","metadata": {"name": "acid-testcluster1"}, "spec": {"podAnnotations": {"foo": "bar"},"teamId": "acid", "clone": {"cluster": "team-batman"}}}`), + annotations: map[string]string{"foo": "bar"}, + err: nil}, +} + func mustParseTime(s string) metav1.Time { v, err := time.Parse("15:04", s) if err != nil { @@ -482,6 +492,25 @@ func TestWeekdayTime(t *testing.T) { } } +func TestClusterAnnotations(t *testing.T) { + for _, tt := range annotations { + var cluster Postgresql + err := cluster.UnmarshalJSON(tt.in) + if err != nil { + if tt.err == nil || err.Error() != tt.err.Error() { + t.Errorf("Unable to marshal cluster with annotations: expected %v got %v", tt.err, err) + } + continue + } + for k, v := range cluster.Spec.PodAnnotations { + found, expected := v, tt.annotations[k] + if found != expected { + t.Errorf("Didn't find correct value for key %v in for podAnnotations: Expected %v found %v", k, expected, found) + } + } + } +} + func TestClusterName(t *testing.T) { for _, tt := range clusterNames { name, err := extractClusterName(tt.in, tt.inTeam) diff --git a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go index 92b2c8704..b68a72d1f 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -81,6 +81,16 @@ func (in *KubernetesMetaConfiguration) DeepCopyInto(out *KubernetesMetaConfigura *out = new(bool) **out = **in } + if in.EnableInitContainers != nil { + in, out := &in.EnableInitContainers, &out.EnableInitContainers + *out = new(bool) + **out = **in + } + if in.EnableSidecars != nil { + in, out := &in.EnableSidecars, &out.EnableSidecars + *out = new(bool) + **out = **in + } out.OAuthTokenSecretName = in.OAuthTokenSecretName out.InfrastructureRolesSecretName = in.InfrastructureRolesSecretName if in.ClusterLabels != nil { @@ -102,6 +112,13 @@ func (in *KubernetesMetaConfiguration) DeepCopyInto(out *KubernetesMetaConfigura (*out)[key] = val } } + if in.CustomPodAnnotations != nil { + in, out := &in.CustomPodAnnotations, &out.CustomPodAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } if in.PodToleration != nil { in, out := &in.PodToleration, &out.PodToleration *out = make(map[string]string, len(*in)) @@ -209,6 +226,11 @@ func (in *OperatorConfiguration) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData) { *out = *in + if in.EnableCRDValidation != nil { + in, out := &in.EnableCRDValidation, &out.EnableCRDValidation + *out = new(bool) + **out = **in + } if in.ShmVolume != nil { in, out := &in.ShmVolume, &out.ShmVolume *out = new(bool) @@ -249,7 +271,7 @@ func (in *OperatorConfigurationData) DeepCopy() *OperatorConfigurationData { func (in *OperatorConfigurationList) DeepCopyInto(out *OperatorConfigurationList) { *out = *in out.TypeMeta = in.TypeMeta - out.ListMeta = in.ListMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]OperatorConfiguration, len(*in)) @@ -278,34 +300,6 @@ func (in *OperatorConfigurationList) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *OperatorConfigurationUsers) DeepCopyInto(out *OperatorConfigurationUsers) { - *out = *in - if in.ProtectedRoles != nil { - in, out := &in.ProtectedRoles, &out.ProtectedRoles - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.TeamAPIRoleConfiguration != nil { - in, out := &in.TeamAPIRoleConfiguration, &out.TeamAPIRoleConfiguration - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatorConfigurationUsers. -func (in *OperatorConfigurationUsers) DeepCopy() *OperatorConfigurationUsers { - if in == nil { - return nil - } - out := new(OperatorConfigurationUsers) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OperatorDebugConfiguration) DeepCopyInto(out *OperatorDebugConfiguration) { *out = *in @@ -513,6 +507,13 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) { *out = new(StandbyDescription) **out = **in } + if in.PodAnnotations != nil { + in, out := &in.PodAnnotations, &out.PodAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } if in.InitContainersOld != nil { in, out := &in.InitContainersOld, &out.InitContainersOld *out = make([]corev1.Container, len(*in)) @@ -597,7 +598,7 @@ func (in *Postgresql) DeepCopyObject() runtime.Object { func (in *PostgresqlList) DeepCopyInto(out *PostgresqlList) { *out = *in out.TypeMeta = in.TypeMeta - out.ListMeta = in.ListMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]Postgresql, len(*in)) diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index e8eed15cc..0a7377389 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -12,7 +12,7 @@ import ( "time" "github.com/sirupsen/logrus" - "k8s.io/api/apps/v1beta1" + appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" policybeta1 "k8s.io/api/policy/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -52,7 +52,7 @@ type kubeResources struct { Services map[PostgresRole]*v1.Service Endpoints map[PostgresRole]*v1.Endpoints Secrets map[types.UID]*v1.Secret - Statefulset *v1beta1.StatefulSet + Statefulset *appsv1.StatefulSet PodDisruptionBudget *policybeta1.PodDisruptionBudget //Pods are treated separately //PVCs are treated separately @@ -164,11 +164,13 @@ func (c *Cluster) setStatus(status string) { } // we cannot do a full scale update here without fetching the previous manifest (as the resourceVersion may differ), - // however, we could do patch without it. In the future, once /status subresource is there (starting Kubernets 1.11) + // however, we could do patch without it. In the future, once /status subresource is there (starting Kubernetes 1.11) // we should take advantage of it. newspec, err := c.KubeClient.AcidV1ClientSet.AcidV1().Postgresqls(c.clusterNamespace()).Patch(c.Name, types.MergePatchType, patch, "status") if err != nil { c.logger.Errorf("could not update status: %v", err) + // return as newspec is empty, see PR654 + return } // update the spec, maintaining the new resourceVersion. c.setSpec(newspec) @@ -212,7 +214,7 @@ func (c *Cluster) Create() error { service *v1.Service ep *v1.Endpoints - ss *v1beta1.StatefulSet + ss *appsv1.StatefulSet ) defer func() { @@ -225,6 +227,10 @@ func (c *Cluster) Create() error { c.setStatus(acidv1.ClusterStatusCreating) + if err = c.validateResources(&c.Spec); err != nil { + return fmt.Errorf("insufficient resource limits specified: %v", err) + } + for _, role := range []PostgresRole{Master, Replica} { if c.Endpoints[role] != nil { @@ -313,7 +319,7 @@ func (c *Cluster) Create() error { return nil } -func (c *Cluster) compareStatefulSetWith(statefulSet *v1beta1.StatefulSet) *compareStatefulsetResult { +func (c *Cluster) compareStatefulSetWith(statefulSet *appsv1.StatefulSet) *compareStatefulsetResult { reasons := make([]string, 0) var match, needsRollUpdate, needsReplace bool @@ -489,6 +495,44 @@ func compareResourcesAssumeFirstNotNil(a *v1.ResourceRequirements, b *v1.Resourc } +func (c *Cluster) validateResources(spec *acidv1.PostgresSpec) error { + + // setting limits too low can cause unnecessary evictions / OOM kills + const ( + cpuMinLimit = "256m" + memoryMinLimit = "256Mi" + ) + + var ( + isSmaller bool + err error + ) + + cpuLimit := spec.Resources.ResourceLimits.CPU + if cpuLimit != "" { + isSmaller, err = util.IsSmallerQuantity(cpuLimit, cpuMinLimit) + if err != nil { + return fmt.Errorf("error validating CPU limit: %v", err) + } + if isSmaller { + return fmt.Errorf("defined CPU limit %s is below required minimum %s to properly run postgresql resource", cpuLimit, cpuMinLimit) + } + } + + memoryLimit := spec.Resources.ResourceLimits.Memory + if memoryLimit != "" { + isSmaller, err = util.IsSmallerQuantity(memoryLimit, memoryMinLimit) + if err != nil { + return fmt.Errorf("error validating memory limit: %v", err) + } + if isSmaller { + return fmt.Errorf("defined memory limit %s is below required minimum %s to properly run postgresql resource", memoryLimit, memoryMinLimit) + } + } + + return nil +} + // Update changes Kubernetes objects according to the new specification. Unlike the sync case, the missing object // (i.e. service) is treated as an error // logical backup cron jobs are an exception: a user-initiated Update can enable a logical backup job @@ -499,6 +543,7 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { c.mu.Lock() defer c.mu.Unlock() + oldStatus := c.Status c.setStatus(acidv1.ClusterStatusUpdating) c.setSpec(newSpec) @@ -510,6 +555,22 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error { } }() + if err := c.validateResources(&newSpec.Spec); err != nil { + err = fmt.Errorf("insufficient resource limits specified: %v", err) + + // cancel update only when (already too low) pod resources were edited + // if cluster was successfully running before the update, continue but log a warning + isCPULimitSmaller, err2 := util.IsSmallerQuantity(newSpec.Spec.Resources.ResourceLimits.CPU, oldSpec.Spec.Resources.ResourceLimits.CPU) + isMemoryLimitSmaller, err3 := util.IsSmallerQuantity(newSpec.Spec.Resources.ResourceLimits.Memory, oldSpec.Spec.Resources.ResourceLimits.Memory) + + if oldStatus.Running() && !isCPULimitSmaller && !isMemoryLimitSmaller && err2 == nil && err3 == nil { + c.logger.Warning(err) + } else { + updateFailed = true + return err + } + } + if oldSpec.Spec.PgVersion != newSpec.Spec.PgVersion { // PG versions comparison c.logger.Warningf("postgresql version change(%q -> %q) has no effect", oldSpec.Spec.PgVersion, newSpec.Spec.PgVersion) //we need that hack to generate statefulset with the old version diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go index 6f10aae22..85d014d8a 100644 --- a/pkg/cluster/cluster_test.go +++ b/pkg/cluster/cluster_test.go @@ -11,7 +11,7 @@ import ( "github.com/zalando/postgres-operator/pkg/util/config" "github.com/zalando/postgres-operator/pkg/util/k8sutil" "github.com/zalando/postgres-operator/pkg/util/teams" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" ) const ( @@ -328,3 +328,57 @@ func TestShouldDeleteSecret(t *testing.T) { } } } + +func TestPodAnnotations(t *testing.T) { + testName := "TestPodAnnotations" + tests := []struct { + subTest string + operator map[string]string + database map[string]string + merged map[string]string + }{ + { + subTest: "No Annotations", + operator: make(map[string]string), + database: make(map[string]string), + merged: make(map[string]string), + }, + { + subTest: "Operator Config Annotations", + operator: map[string]string{"foo": "bar"}, + database: make(map[string]string), + merged: map[string]string{"foo": "bar"}, + }, + { + subTest: "Database Config Annotations", + operator: make(map[string]string), + database: map[string]string{"foo": "bar"}, + merged: map[string]string{"foo": "bar"}, + }, + { + subTest: "Database Config overrides Operator Config Annotations", + operator: map[string]string{"foo": "bar", "global": "foo"}, + database: map[string]string{"foo": "baz", "local": "foo"}, + merged: map[string]string{"foo": "baz", "global": "foo", "local": "foo"}, + }, + } + + for _, tt := range tests { + cl.OpConfig.CustomPodAnnotations = tt.operator + cl.Postgresql.Spec.PodAnnotations = tt.database + + annotations := cl.generatePodAnnotations(&cl.Postgresql.Spec) + for k, v := range annotations { + if observed, expected := v, tt.merged[k]; observed != expected { + t.Errorf("%v expects annotation value %v for key %v, but found %v", + testName+"/"+tt.subTest, expected, observed, k) + } + } + for k, v := range tt.merged { + if observed, expected := annotations[k], v; observed != expected { + t.Errorf("%v expects annotation value %v for key %v, but found %v", + testName+"/"+tt.subTest, expected, observed, k) + } + } + } +} diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 4074c61c6..c69c7a076 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -7,7 +7,7 @@ import ( "github.com/sirupsen/logrus" - "k8s.io/api/apps/v1beta1" + appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" policybeta1 "k8s.io/api/policy/v1beta1" "k8s.io/apimachinery/pkg/api/resource" @@ -430,6 +430,7 @@ func mountShmVolumeNeeded(opConfig config.Config, pgSpec *acidv1.PostgresSpec) * func generatePodTemplate( namespace string, labels labels.Set, + annotations map[string]string, spiloContainer *v1.Container, initContainers []v1.Container, sidecarContainers []v1.Container, @@ -485,13 +486,17 @@ func generatePodTemplate( template := v1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - Namespace: namespace, + Labels: labels, + Namespace: namespace, + Annotations: annotations, }, Spec: podSpec, } if kubeIAMRole != "" { - template.Annotations = map[string]string{constants.KubeIAmAnnotation: kubeIAMRole} + if template.Annotations == nil { + template.Annotations = make(map[string]string) + } + template.Annotations[constants.KubeIAmAnnotation] = kubeIAMRole } return &template, nil @@ -711,10 +716,11 @@ func makeResources(cpuRequest, memoryRequest, cpuLimit, memoryLimit string) acid } } -func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*v1beta1.StatefulSet, error) { +func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.StatefulSet, error) { var ( err error + initContainers []v1.Container sidecarContainers []v1.Container podTemplate *v1.PodTemplateSpec volumeClaimTemplate *v1.PersistentVolumeClaim @@ -735,7 +741,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*v1beta1.State limit = c.OpConfig.DefaultMemoryLimit } - isSmaller, err := util.RequestIsSmallerThanLimit(request, limit) + isSmaller, err := util.IsSmallerQuantity(request, limit) if err != nil { return nil, err } @@ -762,7 +768,7 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*v1beta1.State limit = c.OpConfig.DefaultMemoryLimit } - isSmaller, err := util.RequestIsSmallerThanLimit(sidecarRequest, sidecarLimit) + isSmaller, err := util.IsSmallerQuantity(sidecarRequest, sidecarLimit) if err != nil { return nil, err } @@ -781,6 +787,13 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*v1beta1.State return nil, fmt.Errorf("could not generate resource requirements: %v", err) } + if spec.InitContainers != nil && len(spec.InitContainers) > 0 { + if c.OpConfig.EnableInitContainers != nil && !(*c.OpConfig.EnableInitContainers) { + c.logger.Warningf("initContainers specified but disabled in configuration - next statefulset creation would fail") + } + initContainers = spec.InitContainers + } + customPodEnvVarsList := make([]v1.EnvVar, 0) if c.OpConfig.PodEnvironmentConfigMap != "" { @@ -867,9 +880,14 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*v1beta1.State } // generate sidecar containers - if sidecarContainers, err = generateSidecarContainers(sideCars, volumeMounts, defaultResources, - c.OpConfig.SuperUsername, c.credentialSecretName(c.OpConfig.SuperUsername), c.logger); err != nil { - return nil, fmt.Errorf("could not generate sidecar containers: %v", err) + if sideCars != nil && len(sideCars) > 0 { + if c.OpConfig.EnableSidecars != nil && !(*c.OpConfig.EnableSidecars) { + c.logger.Warningf("sidecars specified but disabled in configuration - next statefulset creation would fail") + } + if sidecarContainers, err = generateSidecarContainers(sideCars, volumeMounts, defaultResources, + c.OpConfig.SuperUsername, c.credentialSecretName(c.OpConfig.SuperUsername), c.logger); err != nil { + return nil, fmt.Errorf("could not generate sidecar containers: %v", err) + } } tolerationSpec := tolerations(&spec.Tolerations, c.OpConfig.PodToleration) @@ -881,12 +899,15 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*v1beta1.State effectiveFSGroup = spec.SpiloFSGroup } + annotations := c.generatePodAnnotations(spec) + // generate pod template for the statefulset, based on the spilo container and sidecars if podTemplate, err = generatePodTemplate( c.Namespace, c.labelsSet(true), + annotations, spiloContainer, - spec.InitContainers, + initContainers, sidecarContainers, &tolerationSpec, effectiveFSGroup, @@ -917,25 +938,25 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*v1beta1.State // the operator has domain-specific logic on how to do rolling updates of PG clusters // so we do not use default rolling updates implemented by stateful sets // that leaves the legacy "OnDelete" update strategy as the only option - updateStrategy := v1beta1.StatefulSetUpdateStrategy{Type: v1beta1.OnDeleteStatefulSetStrategyType} + updateStrategy := appsv1.StatefulSetUpdateStrategy{Type: appsv1.OnDeleteStatefulSetStrategyType} - var podManagementPolicy v1beta1.PodManagementPolicyType + var podManagementPolicy appsv1.PodManagementPolicyType if c.OpConfig.PodManagementPolicy == "ordered_ready" { - podManagementPolicy = v1beta1.OrderedReadyPodManagement + podManagementPolicy = appsv1.OrderedReadyPodManagement } else if c.OpConfig.PodManagementPolicy == "parallel" { - podManagementPolicy = v1beta1.ParallelPodManagement + podManagementPolicy = appsv1.ParallelPodManagement } else { return nil, fmt.Errorf("could not set the pod management policy to the unknown value: %v", c.OpConfig.PodManagementPolicy) } - statefulSet := &v1beta1.StatefulSet{ + statefulSet := &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: c.statefulSetName(), Namespace: c.Namespace, Labels: c.labelsSet(true), Annotations: map[string]string{rollingUpdateStatefulsetAnnotationKey: "false"}, }, - Spec: v1beta1.StatefulSetSpec{ + Spec: appsv1.StatefulSetSpec{ Replicas: &numberOfInstances, Selector: c.labelsSelector(), ServiceName: c.serviceName(Master), @@ -949,6 +970,24 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*v1beta1.State return statefulSet, nil } +func (c *Cluster) generatePodAnnotations(spec *acidv1.PostgresSpec) map[string]string { + annotations := make(map[string]string) + for k, v := range c.OpConfig.CustomPodAnnotations { + annotations[k] = v + } + if spec != nil || spec.PodAnnotations != nil { + for k, v := range spec.PodAnnotations { + annotations[k] = v + } + } + + if len(annotations) == 0 { + return nil + } + + return annotations +} + func generateScalyrSidecarSpec(clusterName, APIKey, serverURL, dockerImage string, containerResources *acidv1.Resources, logger *logrus.Entry) *acidv1.Sidecar { if APIKey == "" || dockerImage == "" { @@ -1386,7 +1425,7 @@ func (c *Cluster) generatePodDisruptionBudget() *policybeta1.PodDisruptionBudget pdbEnabled := c.OpConfig.EnablePodDisruptionBudget // if PodDisruptionBudget is disabled or if there are no DB pods, set the budget to 0. - if (pdbEnabled != nil && !*pdbEnabled) || c.Spec.NumberOfInstances <= 0 { + if (pdbEnabled != nil && !(*pdbEnabled)) || c.Spec.NumberOfInstances <= 0 { minAvailable = intstr.FromInt(0) } @@ -1462,10 +1501,13 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1beta1.CronJob, error) { }, }} + annotations := c.generatePodAnnotations(&c.Spec) + // re-use the method that generates DB pod templates if podTemplate, err = generatePodTemplate( c.Namespace, labels, + annotations, logicalBackupContainer, []v1.Container{}, []v1.Container{}, @@ -1479,8 +1521,8 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1beta1.CronJob, error) { util.False(), false, "", - "", - ""); err != nil { + c.OpConfig.AdditionalSecretMount, + c.OpConfig.AdditionalSecretMountPath); err != nil { return nil, fmt.Errorf("could not generate pod template for logical backup pod: %v", err) } @@ -1528,6 +1570,10 @@ func (c *Cluster) generateLogicalBackupPodEnvVars() []v1.EnvVar { Name: "SCOPE", Value: c.Name, }, + { + Name: "CLUSTER_NAME_LABEL", + Value: c.OpConfig.ClusterNameLabel, + }, { Name: "POD_NAMESPACE", ValueFrom: &v1.EnvVarSource{ @@ -1542,6 +1588,14 @@ func (c *Cluster) generateLogicalBackupPodEnvVars() []v1.EnvVar { Name: "LOGICAL_BACKUP_S3_BUCKET", Value: c.OpConfig.LogicalBackup.LogicalBackupS3Bucket, }, + { + Name: "LOGICAL_BACKUP_S3_ENDPOINT", + Value: c.OpConfig.LogicalBackup.LogicalBackupS3Endpoint, + }, + { + Name: "LOGICAL_BACKUP_S3_SSE", + Value: c.OpConfig.LogicalBackup.LogicalBackupS3SSE, + }, { Name: "LOGICAL_BACKUP_S3_BUCKET_SCOPE_SUFFIX", Value: getBucketScopeSuffix(string(c.Postgresql.GetUID())), @@ -1580,8 +1634,15 @@ func (c *Cluster) generateLogicalBackupPodEnvVars() []v1.EnvVar { }, } - c.logger.Debugf("Generated logical backup env vars %v", envVars) + if c.OpConfig.LogicalBackup.LogicalBackupS3AccessKeyID != "" { + envVars = append(envVars, v1.EnvVar{Name: "AWS_ACCESS_KEY_ID", Value: c.OpConfig.LogicalBackup.LogicalBackupS3AccessKeyID}) + } + if c.OpConfig.LogicalBackup.LogicalBackupS3SecretAccessKey != "" { + envVars = append(envVars, v1.EnvVar{Name: "AWS_SECRET_ACCESS_KEY", Value: c.OpConfig.LogicalBackup.LogicalBackupS3SecretAccessKey}) + } + + c.logger.Debugf("Generated logical backup env vars %v", envVars) return envVars } diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index 5b206f760..e8fe05456 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -3,7 +3,7 @@ package cluster import ( "reflect" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "testing" diff --git a/pkg/cluster/pod.go b/pkg/cluster/pod.go index 8beb561ec..095f859f0 100644 --- a/pkg/cluster/pod.go +++ b/pkg/cluster/pod.go @@ -4,12 +4,12 @@ import ( "fmt" "math/rand" + appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/util" - "k8s.io/api/apps/v1beta1" ) func (c *Cluster) listPods() ([]v1.Pod, error) { @@ -205,7 +205,7 @@ func (c *Cluster) MigrateMasterPod(podName spec.NamespacedName) error { } // we must have a statefulset in the cluster for the migration to work if c.Statefulset == nil { - var sset *v1beta1.StatefulSet + var sset *appsv1.StatefulSet if sset, err = c.KubeClient.StatefulSets(c.Namespace).Get(c.statefulSetName(), metav1.GetOptions{}); err != nil { return fmt.Errorf("could not retrieve cluster statefulset: %v", err) diff --git a/pkg/cluster/resources.go b/pkg/cluster/resources.go index c89edac63..c94a7bb46 100644 --- a/pkg/cluster/resources.go +++ b/pkg/cluster/resources.go @@ -5,7 +5,7 @@ import ( "strconv" "strings" - "k8s.io/api/apps/v1beta1" + appsv1 "k8s.io/api/apps/v1" batchv1beta1 "k8s.io/api/batch/v1beta1" v1 "k8s.io/api/core/v1" policybeta1 "k8s.io/api/policy/v1beta1" @@ -13,7 +13,6 @@ import ( "k8s.io/apimachinery/pkg/types" "github.com/zalando/postgres-operator/pkg/util" - "github.com/zalando/postgres-operator/pkg/util/constants" "github.com/zalando/postgres-operator/pkg/util/k8sutil" "github.com/zalando/postgres-operator/pkg/util/retryutil" ) @@ -64,8 +63,19 @@ func (c *Cluster) listResources() error { return nil } -func (c *Cluster) createStatefulSet() (*v1beta1.StatefulSet, error) { +func (c *Cluster) createStatefulSet() (*appsv1.StatefulSet, error) { c.setProcessName("creating statefulset") + // check if it's allowed that spec contains initContainers + if c.Spec.InitContainers != nil && len(c.Spec.InitContainers) > 0 && + c.OpConfig.EnableInitContainers != nil && !(*c.OpConfig.EnableInitContainers) { + return nil, fmt.Errorf("initContainers specified but disabled in configuration") + } + // check if it's allowed that spec contains sidecars + if c.Spec.Sidecars != nil && len(c.Spec.Sidecars) > 0 && + c.OpConfig.EnableSidecars != nil && !(*c.OpConfig.EnableSidecars) { + return nil, fmt.Errorf("sidecar containers specified but disabled in configuration") + } + statefulSetSpec, err := c.generateStatefulSet(&c.Spec) if err != nil { return nil, fmt.Errorf("could not generate statefulset: %v", err) @@ -95,7 +105,7 @@ func getPodIndex(podName string) (int32, error) { return int32(res), nil } -func (c *Cluster) preScaleDown(newStatefulSet *v1beta1.StatefulSet) error { +func (c *Cluster) preScaleDown(newStatefulSet *appsv1.StatefulSet) error { masterPod, err := c.getRolePods(Master) if err != nil { return fmt.Errorf("could not get master pod: %v", err) @@ -135,7 +145,7 @@ func (c *Cluster) preScaleDown(newStatefulSet *v1beta1.StatefulSet) error { // setRollingUpdateFlagForStatefulSet sets the indicator or the rolling update requirement // in the StatefulSet annotation. -func (c *Cluster) setRollingUpdateFlagForStatefulSet(sset *v1beta1.StatefulSet, val bool) { +func (c *Cluster) setRollingUpdateFlagForStatefulSet(sset *appsv1.StatefulSet, val bool) { anno := sset.GetAnnotations() if anno == nil { anno = make(map[string]string) @@ -160,7 +170,7 @@ func (c *Cluster) applyRollingUpdateFlagforStatefulSet(val bool) error { // getRollingUpdateFlagFromStatefulSet returns the value of the rollingUpdate flag from the passed // StatefulSet, reverting to the default value in case of errors -func (c *Cluster) getRollingUpdateFlagFromStatefulSet(sset *v1beta1.StatefulSet, defaultValue bool) (flag bool) { +func (c *Cluster) getRollingUpdateFlagFromStatefulSet(sset *appsv1.StatefulSet, defaultValue bool) (flag bool) { anno := sset.GetAnnotations() flag = defaultValue @@ -181,7 +191,7 @@ func (c *Cluster) getRollingUpdateFlagFromStatefulSet(sset *v1beta1.StatefulSet, // mergeRollingUpdateFlagUsingCache returns the value of the rollingUpdate flag from the passed // statefulset, however, the value can be cleared if there is a cached flag in the cluster that // is set to false (the discrepancy could be a result of a failed StatefulSet update) -func (c *Cluster) mergeRollingUpdateFlagUsingCache(runningStatefulSet *v1beta1.StatefulSet) bool { +func (c *Cluster) mergeRollingUpdateFlagUsingCache(runningStatefulSet *appsv1.StatefulSet) bool { var ( cachedStatefulsetExists, clearRollingUpdateFromCache, podsRollingUpdateRequired bool ) @@ -207,7 +217,7 @@ func (c *Cluster) mergeRollingUpdateFlagUsingCache(runningStatefulSet *v1beta1.S return podsRollingUpdateRequired } -func (c *Cluster) updateStatefulSetAnnotations(annotations map[string]string) (*v1beta1.StatefulSet, error) { +func (c *Cluster) updateStatefulSetAnnotations(annotations map[string]string) (*appsv1.StatefulSet, error) { c.logger.Debugf("updating statefulset annotations") patchData, err := metaAnnotationsPatch(annotations) if err != nil { @@ -223,7 +233,7 @@ func (c *Cluster) updateStatefulSetAnnotations(annotations map[string]string) (* return result, nil } -func (c *Cluster) updateStatefulSet(newStatefulSet *v1beta1.StatefulSet) error { +func (c *Cluster) updateStatefulSet(newStatefulSet *appsv1.StatefulSet) error { c.setProcessName("updating statefulset") if c.Statefulset == nil { return fmt.Errorf("there is no statefulset in the cluster") @@ -264,7 +274,7 @@ func (c *Cluster) updateStatefulSet(newStatefulSet *v1beta1.StatefulSet) error { } // replaceStatefulSet deletes an old StatefulSet and creates the new using spec in the PostgreSQL CRD. -func (c *Cluster) replaceStatefulSet(newStatefulSet *v1beta1.StatefulSet) error { +func (c *Cluster) replaceStatefulSet(newStatefulSet *appsv1.StatefulSet) error { c.setProcessName("replacing statefulset") if c.Statefulset == nil { return fmt.Errorf("there is no statefulset in the cluster") @@ -278,7 +288,8 @@ func (c *Cluster) replaceStatefulSet(newStatefulSet *v1beta1.StatefulSet) error oldStatefulset := c.Statefulset options := metav1.DeleteOptions{PropagationPolicy: &deletePropagationPolicy} - if err := c.KubeClient.StatefulSets(oldStatefulset.Namespace).Delete(oldStatefulset.Name, &options); err != nil { + err := c.KubeClient.StatefulSets(oldStatefulset.Namespace).Delete(oldStatefulset.Name, &options) + if err != nil { return fmt.Errorf("could not delete statefulset %q: %v", statefulSetName, err) } // make sure we clear the stored statefulset status if the subsequent create fails. @@ -286,11 +297,16 @@ func (c *Cluster) replaceStatefulSet(newStatefulSet *v1beta1.StatefulSet) error // wait until the statefulset is truly deleted c.logger.Debugf("waiting for the statefulset to be deleted") - err := retryutil.Retry(constants.StatefulsetDeletionInterval, constants.StatefulsetDeletionTimeout, + err = retryutil.Retry(c.OpConfig.ResourceCheckInterval, c.OpConfig.ResourceCheckTimeout, func() (bool, error) { - _, err := c.KubeClient.StatefulSets(oldStatefulset.Namespace).Get(oldStatefulset.Name, metav1.GetOptions{}) - - return err != nil, nil + _, err2 := c.KubeClient.StatefulSets(oldStatefulset.Namespace).Get(oldStatefulset.Name, metav1.GetOptions{}) + if err2 == nil { + return false, nil + } + if k8sutil.ResourceNotFound(err2) { + return true, nil + } + return false, err2 }) if err != nil { return fmt.Errorf("could not delete statefulset: %v", err) @@ -329,7 +345,7 @@ func (c *Cluster) deleteStatefulSet() error { return fmt.Errorf("could not delete pods: %v", err) } - if err := c.deletePersistenVolumeClaims(); err != nil { + if err := c.deletePersistentVolumeClaims(); err != nil { return fmt.Errorf("could not delete PersistentVolumeClaims: %v", err) } @@ -380,13 +396,27 @@ func (c *Cluster) updateService(role PostgresRole, newService *v1.Service) error return fmt.Errorf("could not delete service %q: %v", serviceName, err) } - c.Endpoints[role] = nil - svc, err := c.KubeClient.Services(serviceName.Namespace).Create(newService) + // wait until the service is truly deleted + c.logger.Debugf("waiting for service to be deleted") + + err = retryutil.Retry(c.OpConfig.ResourceCheckInterval, c.OpConfig.ResourceCheckTimeout, + func() (bool, error) { + _, err2 := c.KubeClient.Services(serviceName.Namespace).Get(serviceName.Name, metav1.GetOptions{}) + if err2 == nil { + return false, nil + } + if k8sutil.ResourceNotFound(err2) { + return true, nil + } + return false, err2 + }) if err != nil { - return fmt.Errorf("could not create service %q: %v", serviceName, err) + return fmt.Errorf("could not delete service %q: %v", serviceName, err) } - c.Services[role] = svc + // make sure we clear the stored service and endpoint status if the subsequent create fails. + c.Services[role] = nil + c.Endpoints[role] = nil if role == Master { // create the new endpoint using the addresses obtained from the previous one endpointSpec := c.generateEndpoint(role, currentEndpoint.Subsets) @@ -398,6 +428,13 @@ func (c *Cluster) updateService(role PostgresRole, newService *v1.Service) error c.Endpoints[role] = ep } + svc, err := c.KubeClient.Services(serviceName.Namespace).Create(newService) + if err != nil { + return fmt.Errorf("could not create service %q: %v", serviceName, err) + } + + c.Services[role] = svc + return nil } @@ -676,7 +713,7 @@ func (c *Cluster) GetEndpointReplica() *v1.Endpoints { } // GetStatefulSet returns cluster's kubernetes StatefulSet -func (c *Cluster) GetStatefulSet() *v1beta1.StatefulSet { +func (c *Cluster) GetStatefulSet() *appsv1.StatefulSet { return c.Statefulset } diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index dd55cd04c..abe579fb5 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -23,6 +23,7 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error { c.mu.Lock() defer c.mu.Unlock() + oldStatus := c.Status c.setSpec(newSpec) defer func() { @@ -34,6 +35,16 @@ func (c *Cluster) Sync(newSpec *acidv1.Postgresql) error { } }() + if err = c.validateResources(&c.Spec); err != nil { + err = fmt.Errorf("insufficient resource limits specified: %v", err) + if oldStatus.Running() { + c.logger.Warning(err) + err = nil + } else { + return err + } + } + if err = c.initUsers(); err != nil { err = fmt.Errorf("could not init users: %v", err) return err diff --git a/pkg/cluster/types.go b/pkg/cluster/types.go index 5fd5029db..138b7015c 100644 --- a/pkg/cluster/types.go +++ b/pkg/cluster/types.go @@ -4,8 +4,8 @@ import ( "time" acidv1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" - "k8s.io/api/apps/v1beta1" - "k8s.io/api/core/v1" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" policybeta1 "k8s.io/api/policy/v1beta1" "k8s.io/apimachinery/pkg/types" ) @@ -60,7 +60,7 @@ type ClusterStatus struct { ReplicaService *v1.Service MasterEndpoint *v1.Endpoints ReplicaEndpoint *v1.Endpoints - StatefulSet *v1beta1.StatefulSet + StatefulSet *appsv1.StatefulSet PodDisruptionBudget *policybeta1.PodDisruptionBudget CurrentProcess Process diff --git a/pkg/cluster/util.go b/pkg/cluster/util.go index 5b531cc90..8c02fed2e 100644 --- a/pkg/cluster/util.go +++ b/pkg/cluster/util.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "k8s.io/api/apps/v1beta1" + appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" policybeta1 "k8s.io/api/policy/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -168,7 +168,7 @@ func (c *Cluster) logPDBChanges(old, new *policybeta1.PodDisruptionBudget, isUpd c.logger.Debugf("diff\n%s\n", util.PrettyDiff(old.Spec, new.Spec)) } -func (c *Cluster) logStatefulSetChanges(old, new *v1beta1.StatefulSet, isUpdate bool, reasons []string) { +func (c *Cluster) logStatefulSetChanges(old, new *appsv1.StatefulSet, isUpdate bool, reasons []string) { if isUpdate { c.logger.Infof("statefulset %q has been changed", util.NameFromMeta(old.ObjectMeta)) } else { @@ -365,7 +365,7 @@ func (c *Cluster) waitStatefulsetPodsReady() error { c.setProcessName("waiting for the pods of the statefulset") // TODO: wait for the first Pod only if err := c.waitStatefulsetReady(); err != nil { - return fmt.Errorf("statuful set error: %v", err) + return fmt.Errorf("stateful set error: %v", err) } // TODO: wait only for master diff --git a/pkg/cluster/volumes.go b/pkg/cluster/volumes.go index 57f8ca7e3..d92ae6258 100644 --- a/pkg/cluster/volumes.go +++ b/pkg/cluster/volumes.go @@ -30,7 +30,7 @@ func (c *Cluster) listPersistentVolumeClaims() ([]v1.PersistentVolumeClaim, erro return pvcs.Items, nil } -func (c *Cluster) deletePersistenVolumeClaims() error { +func (c *Cluster) deletePersistentVolumeClaims() error { c.logger.Debugln("deleting PVCs") pvcs, err := c.listPersistentVolumeClaims() if err != nil { diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index a492a85e2..831078f3e 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -6,7 +6,7 @@ import ( "sync" "github.com/sirupsen/logrus" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" rbacv1beta1 "k8s.io/api/rbac/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -58,7 +58,6 @@ type Controller struct { PodServiceAccount *v1.ServiceAccount PodServiceAccountRoleBinding *rbacv1beta1.RoleBinding - namespacesWithDefinedRBAC sync.Map } // NewController creates a new controller @@ -112,7 +111,7 @@ func (c *Controller) initOperatorConfig() { if c.opConfig.SetMemoryRequestToLimit { - isSmaller, err := util.RequestIsSmallerThanLimit(c.opConfig.DefaultMemoryRequest, c.opConfig.DefaultMemoryLimit) + isSmaller, err := util.IsSmallerQuantity(c.opConfig.DefaultMemoryRequest, c.opConfig.DefaultMemoryLimit) if err != nil { panic(err) } @@ -121,7 +120,7 @@ func (c *Controller) initOperatorConfig() { c.opConfig.DefaultMemoryRequest = c.opConfig.DefaultMemoryLimit } - isSmaller, err = util.RequestIsSmallerThanLimit(c.opConfig.ScalyrMemoryRequest, c.opConfig.ScalyrMemoryLimit) + isSmaller, err = util.IsSmallerQuantity(c.opConfig.ScalyrMemoryRequest, c.opConfig.ScalyrMemoryLimit) if err != nil { panic(err) } @@ -241,7 +240,7 @@ func (c *Controller) initController() { c.initClients() if configObjectName := os.Getenv("POSTGRES_OPERATOR_CONFIGURATION_OBJECT"); configObjectName != "" { - if err := c.createConfigurationCRD(); err != nil { + if err := c.createConfigurationCRD(c.opConfig.EnableCRDValidation); err != nil { c.logger.Fatalf("could not register Operator Configuration CustomResourceDefinition: %v", err) } if cfg, err := c.readOperatorConfigurationFromCRD(spec.GetOperatorNamespace(), configObjectName); err != nil { @@ -257,7 +256,7 @@ func (c *Controller) initController() { c.modifyConfigFromEnvironment() - if err := c.createPostgresCRD(); err != nil { + if err := c.createPostgresCRD(c.opConfig.EnableCRDValidation); err != nil { c.logger.Fatalf("could not register Postgres CustomResourceDefinition: %v", err) } diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 9da4bbaf8..56ba91d02 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -25,6 +25,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result := &config.Config{} // general config + result.EnableCRDValidation = fromCRD.EnableCRDValidation result.EtcdHost = fromCRD.EtcdHost result.DockerImage = fromCRD.DockerImage result.Workers = fromCRD.Workers @@ -41,6 +42,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.ReplicationUsername = fromCRD.PostgresUsersConfiguration.ReplicationUsername // kubernetes config + result.CustomPodAnnotations = fromCRD.Kubernetes.CustomPodAnnotations result.PodServiceAccountName = fromCRD.Kubernetes.PodServiceAccountName result.PodServiceAccountDefinition = fromCRD.Kubernetes.PodServiceAccountDefinition result.PodServiceAccountRoleBindingDefinition = fromCRD.Kubernetes.PodServiceAccountRoleBindingDefinition @@ -52,6 +54,8 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.WatchedNamespace = fromCRD.Kubernetes.WatchedNamespace result.PDBNameFormat = fromCRD.Kubernetes.PDBNameFormat result.EnablePodDisruptionBudget = fromCRD.Kubernetes.EnablePodDisruptionBudget + result.EnableInitContainers = fromCRD.Kubernetes.EnableInitContainers + result.EnableSidecars = fromCRD.Kubernetes.EnableSidecars result.SecretNameTemplate = fromCRD.Kubernetes.SecretNameTemplate result.OAuthTokenSecretName = fromCRD.Kubernetes.OAuthTokenSecretName result.InfrastructureRolesSecretName = fromCRD.Kubernetes.InfrastructureRolesSecretName @@ -100,6 +104,10 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur result.LogicalBackupSchedule = fromCRD.LogicalBackup.Schedule result.LogicalBackupDockerImage = fromCRD.LogicalBackup.DockerImage result.LogicalBackupS3Bucket = fromCRD.LogicalBackup.S3Bucket + result.LogicalBackupS3Endpoint = fromCRD.LogicalBackup.S3Endpoint + result.LogicalBackupS3AccessKeyID = fromCRD.LogicalBackup.S3AccessKeyID + result.LogicalBackupS3SecretAccessKey = fromCRD.LogicalBackup.S3SecretAccessKey + result.LogicalBackupS3SSE = fromCRD.LogicalBackup.S3SSE // debug config result.DebugLogging = fromCRD.OperatorDebug.DebugLogging diff --git a/pkg/controller/postgresql.go b/pkg/controller/postgresql.go index dc082ffab..8e8f9ae85 100644 --- a/pkg/controller/postgresql.go +++ b/pkg/controller/postgresql.go @@ -312,7 +312,7 @@ func (c *Controller) processClusterEventsQueue(idx int, stopCh <-chan struct{}, for { obj, err := c.clusterEventQueues[idx].Pop(cache.PopProcessFunc(func(interface{}) error { return nil })) if err != nil { - if err == cache.FIFOClosedError { + if err == cache.ErrFIFOClosed { return } c.logger.Errorf("error when processing cluster events queue: %v", err) @@ -493,17 +493,16 @@ func (c *Controller) postgresqlDelete(obj interface{}) { } /* - Ensures the pod service account and role bindings exists in a namespace before a PG cluster is created there so that a user does not have to deploy these credentials manually. - StatefulSets require the service account to create pods; Patroni requires relevant RBAC bindings to access endpoints. + Ensures the pod service account and role bindings exists in a namespace + before a PG cluster is created there so that a user does not have to deploy + these credentials manually. StatefulSets require the service account to + create pods; Patroni requires relevant RBAC bindings to access endpoints. The operator does not sync accounts/role bindings after creation. */ func (c *Controller) submitRBACCredentials(event ClusterEvent) error { namespace := event.NewSpec.GetNamespace() - if _, ok := c.namespacesWithDefinedRBAC.Load(namespace); ok { - return nil - } if err := c.createPodServiceAccount(namespace); err != nil { return fmt.Errorf("could not create pod service account %v : %v", c.opConfig.PodServiceAccountName, err) @@ -512,7 +511,6 @@ func (c *Controller) submitRBACCredentials(event ClusterEvent) error { if err := c.createRoleBindings(namespace); err != nil { return fmt.Errorf("could not create role binding %v : %v", c.PodServiceAccountRoleBinding.Name, err) } - c.namespacesWithDefinedRBAC.Store(namespace, true) return nil } diff --git a/pkg/controller/util.go b/pkg/controller/util.go index 0adb85dbd..9b7dca063 100644 --- a/pkg/controller/util.go +++ b/pkg/controller/util.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -91,12 +91,12 @@ func (c *Controller) createOperatorCRD(crd *apiextv1beta1.CustomResourceDefiniti }) } -func (c *Controller) createPostgresCRD() error { - return c.createOperatorCRD(acidv1.PostgresCRD()) +func (c *Controller) createPostgresCRD(enableValidation *bool) error { + return c.createOperatorCRD(acidv1.PostgresCRD(enableValidation)) } -func (c *Controller) createConfigurationCRD() error { - return c.createOperatorCRD(acidv1.ConfigurationCRD()) +func (c *Controller) createConfigurationCRD(enableValidation *bool) error { + return c.createOperatorCRD(acidv1.ConfigurationCRD(enableValidation)) } func readDecodedRole(s string) (*spec.PgUser, error) { diff --git a/pkg/controller/util_test.go b/pkg/controller/util_test.go index c9e16cbd9..a5d3c7ac5 100644 --- a/pkg/controller/util_test.go +++ b/pkg/controller/util_test.go @@ -9,7 +9,7 @@ import ( "github.com/zalando/postgres-operator/pkg/spec" "github.com/zalando/postgres-operator/pkg/util/k8sutil" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) diff --git a/pkg/generated/clientset/versioned/clientset.go b/pkg/generated/clientset/versioned/clientset.go index 6969f794c..45c1893a8 100644 --- a/pkg/generated/clientset/versioned/clientset.go +++ b/pkg/generated/clientset/versioned/clientset.go @@ -25,6 +25,8 @@ SOFTWARE. package versioned import ( + "fmt" + acidv1 "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1" discovery "k8s.io/client-go/discovery" rest "k8s.io/client-go/rest" @@ -34,8 +36,6 @@ import ( type Interface interface { Discovery() discovery.DiscoveryInterface AcidV1() acidv1.AcidV1Interface - // Deprecated: please explicitly pick a version if possible. - Acid() acidv1.AcidV1Interface } // Clientset contains the clients for groups. Each group has exactly one @@ -50,12 +50,6 @@ func (c *Clientset) AcidV1() acidv1.AcidV1Interface { return c.acidV1 } -// Deprecated: Acid retrieves the default version of AcidClient. -// Please explicitly pick a version. -func (c *Clientset) Acid() acidv1.AcidV1Interface { - return c.acidV1 -} - // Discovery retrieves the DiscoveryClient func (c *Clientset) Discovery() discovery.DiscoveryInterface { if c == nil { @@ -65,9 +59,14 @@ func (c *Clientset) Discovery() discovery.DiscoveryInterface { } // NewForConfig creates a new Clientset for the given config. +// If config's RateLimiter is not set and QPS and Burst are acceptable, +// NewForConfig will generate a rate-limiter in configShallowCopy. func NewForConfig(c *rest.Config) (*Clientset, error) { configShallowCopy := *c if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { + if configShallowCopy.Burst <= 0 { + return nil, fmt.Errorf("Burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") + } configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) } var cs Clientset diff --git a/pkg/generated/clientset/versioned/fake/clientset_generated.go b/pkg/generated/clientset/versioned/fake/clientset_generated.go index 861059b11..fd63d0ba4 100644 --- a/pkg/generated/clientset/versioned/fake/clientset_generated.go +++ b/pkg/generated/clientset/versioned/fake/clientset_generated.go @@ -47,7 +47,7 @@ func NewSimpleClientset(objects ...runtime.Object) *Clientset { } } - cs := &Clientset{} + cs := &Clientset{tracker: o} cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} cs.AddReactor("*", "*", testing.ObjectReaction(o)) cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { @@ -69,20 +69,20 @@ func NewSimpleClientset(objects ...runtime.Object) *Clientset { type Clientset struct { testing.Fake discovery *fakediscovery.FakeDiscovery + tracker testing.ObjectTracker } func (c *Clientset) Discovery() discovery.DiscoveryInterface { return c.discovery } +func (c *Clientset) Tracker() testing.ObjectTracker { + return c.tracker +} + var _ clientset.Interface = &Clientset{} // AcidV1 retrieves the AcidV1Client func (c *Clientset) AcidV1() acidv1.AcidV1Interface { return &fakeacidv1.FakeAcidV1{Fake: &c.Fake} } - -// Acid retrieves the AcidV1Client -func (c *Clientset) Acid() acidv1.AcidV1Interface { - return &fakeacidv1.FakeAcidV1{Fake: &c.Fake} -} diff --git a/pkg/generated/clientset/versioned/fake/register.go b/pkg/generated/clientset/versioned/fake/register.go index 8e6179239..2883940ca 100644 --- a/pkg/generated/clientset/versioned/fake/register.go +++ b/pkg/generated/clientset/versioned/fake/register.go @@ -30,15 +30,14 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) var scheme = runtime.NewScheme() var codecs = serializer.NewCodecFactory(scheme) var parameterCodec = runtime.NewParameterCodec(scheme) - -func init() { - v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) - AddToScheme(scheme) +var localSchemeBuilder = runtime.SchemeBuilder{ + acidv1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition @@ -51,10 +50,13 @@ func init() { // ) // // kclientset, _ := kubernetes.NewForConfig(c) -// aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // correctly. -func AddToScheme(scheme *runtime.Scheme) { - acidv1.AddToScheme(scheme) +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(scheme)) } diff --git a/pkg/generated/clientset/versioned/scheme/register.go b/pkg/generated/clientset/versioned/scheme/register.go index eec31f1c7..acdd09c25 100644 --- a/pkg/generated/clientset/versioned/scheme/register.go +++ b/pkg/generated/clientset/versioned/scheme/register.go @@ -30,15 +30,14 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" schema "k8s.io/apimachinery/pkg/runtime/schema" serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) var Scheme = runtime.NewScheme() var Codecs = serializer.NewCodecFactory(Scheme) var ParameterCodec = runtime.NewParameterCodec(Scheme) - -func init() { - v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) - AddToScheme(Scheme) +var localSchemeBuilder = runtime.SchemeBuilder{ + acidv1.AddToScheme, } // AddToScheme adds all types of this clientset into the given scheme. This allows composition @@ -51,10 +50,13 @@ func init() { // ) // // kclientset, _ := kubernetes.NewForConfig(c) -// aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) // // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types // correctly. -func AddToScheme(scheme *runtime.Scheme) { - acidv1.AddToScheme(scheme) +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(Scheme)) } diff --git a/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/acid.zalan.do_client.go b/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/acid.zalan.do_client.go index cf324cc2d..388ec6178 100644 --- a/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/acid.zalan.do_client.go +++ b/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/acid.zalan.do_client.go @@ -27,7 +27,6 @@ package v1 import ( v1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/scheme" - serializer "k8s.io/apimachinery/pkg/runtime/serializer" rest "k8s.io/client-go/rest" ) @@ -82,7 +81,7 @@ func setConfigDefaults(config *rest.Config) error { gv := v1.SchemeGroupVersion config.GroupVersion = &gv config.APIPath = "/apis" - config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs} + config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() if config.UserAgent == "" { config.UserAgent = rest.DefaultKubernetesUserAgent() diff --git a/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/fake/fake_postgresql.go b/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/fake/fake_postgresql.go index d71c0f076..8cc50e598 100644 --- a/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/fake/fake_postgresql.go +++ b/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/fake/fake_postgresql.go @@ -137,7 +137,7 @@ func (c *FakePostgresqls) DeleteCollection(options *v1.DeleteOptions, listOption // Patch applies the patch and returns the patched postgresql. func (c *FakePostgresqls) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *acidzalandov1.Postgresql, err error) { obj, err := c.Fake. - Invokes(testing.NewPatchSubresourceAction(postgresqlsResource, c.ns, name, data, subresources...), &acidzalandov1.Postgresql{}) + Invokes(testing.NewPatchSubresourceAction(postgresqlsResource, c.ns, name, pt, data, subresources...), &acidzalandov1.Postgresql{}) if obj == nil { return nil, err diff --git a/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/postgresql.go b/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/postgresql.go index 9afad20f5..88645dab6 100644 --- a/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/postgresql.go +++ b/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1/postgresql.go @@ -25,6 +25,8 @@ SOFTWARE. package v1 import ( + "time" + v1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" scheme "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/scheme" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -82,11 +84,16 @@ func (c *postgresqls) Get(name string, options metav1.GetOptions) (result *v1.Po // List takes label and field selectors, and returns the list of Postgresqls that match those selectors. func (c *postgresqls) List(opts metav1.ListOptions) (result *v1.PostgresqlList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } result = &v1.PostgresqlList{} err = c.client.Get(). Namespace(c.ns). Resource("postgresqls"). VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). Do(). Into(result) return @@ -94,11 +101,16 @@ func (c *postgresqls) List(opts metav1.ListOptions) (result *v1.PostgresqlList, // Watch returns a watch.Interface that watches the requested postgresqls. func (c *postgresqls) Watch(opts metav1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } opts.Watch = true return c.client.Get(). Namespace(c.ns). Resource("postgresqls"). VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). Watch() } @@ -156,10 +168,15 @@ func (c *postgresqls) Delete(name string, options *metav1.DeleteOptions) error { // DeleteCollection deletes a collection of objects. func (c *postgresqls) DeleteCollection(options *metav1.DeleteOptions, listOptions metav1.ListOptions) error { + var timeout time.Duration + if listOptions.TimeoutSeconds != nil { + timeout = time.Duration(*listOptions.TimeoutSeconds) * time.Second + } return c.client.Delete(). Namespace(c.ns). Resource("postgresqls"). VersionedParams(&listOptions, scheme.ParameterCodec). + Timeout(timeout). Body(options). Do(). Error() diff --git a/pkg/generated/informers/externalversions/internalinterfaces/factory_interfaces.go b/pkg/generated/informers/externalversions/internalinterfaces/factory_interfaces.go index bdd892f35..712305087 100644 --- a/pkg/generated/informers/externalversions/internalinterfaces/factory_interfaces.go +++ b/pkg/generated/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -33,6 +33,7 @@ import ( cache "k8s.io/client-go/tools/cache" ) +// NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer // SharedInformerFactory a small interface to allow for adding an informer without an import cycle @@ -41,4 +42,5 @@ type SharedInformerFactory interface { InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer } +// TweakListOptionsFunc is a function that transforms a v1.ListOptions. type TweakListOptionsFunc func(*v1.ListOptions) diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index 48e4a700f..224691120 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -12,10 +12,11 @@ import ( // CRD describes CustomResourceDefinition specific configuration parameters type CRD struct { - ReadyWaitInterval time.Duration `name:"ready_wait_interval" default:"4s"` - ReadyWaitTimeout time.Duration `name:"ready_wait_timeout" default:"30s"` - ResyncPeriod time.Duration `name:"resync_period" default:"30m"` - RepairPeriod time.Duration `name:"repair_period" default:"5m"` + ReadyWaitInterval time.Duration `name:"ready_wait_interval" default:"4s"` + ReadyWaitTimeout time.Duration `name:"ready_wait_timeout" default:"30s"` + ResyncPeriod time.Duration `name:"resync_period" default:"30m"` + RepairPeriod time.Duration `name:"repair_period" default:"5m"` + EnableCRDValidation *bool `name:"enable_crd_validation" default:"true"` } // Resources describes kubernetes resource specific configuration parameters @@ -68,11 +69,15 @@ type Scalyr struct { ScalyrMemoryLimit string `name:"scalyr_memory_limit" default:"1Gi"` } -// LogicalBackup +// LogicalBackup defines configuration for logical backup type LogicalBackup struct { - LogicalBackupSchedule string `name:"logical_backup_schedule" default:"30 00 * * *"` - LogicalBackupDockerImage string `name:"logical_backup_docker_image" default:"registry.opensource.zalan.do/acid/logical-backup"` - LogicalBackupS3Bucket string `name:"logical_backup_s3_bucket" default:""` + LogicalBackupSchedule string `name:"logical_backup_schedule" default:"30 00 * * *"` + LogicalBackupDockerImage string `name:"logical_backup_docker_image" default:"registry.opensource.zalan.do/acid/logical-backup"` + LogicalBackupS3Bucket string `name:"logical_backup_s3_bucket" default:""` + LogicalBackupS3Endpoint string `name:"logical_backup_s3_endpoint" default:""` + LogicalBackupS3AccessKeyID string `name:"logical_backup_s3_access_key_id" default:""` + LogicalBackupS3SecretAccessKey string `name:"logical_backup_s3_secret_access_key" default:""` + LogicalBackupS3SSE string `name:"logical_backup_s3_sse" default:"AES256"` } // Config describes operator config @@ -85,7 +90,7 @@ type Config struct { WatchedNamespace string `name:"watched_namespace"` // special values: "*" means 'watch all namespaces', the empty string "" means 'watch a namespace where operator is deployed to' EtcdHost string `name:"etcd_host" default:""` // special values: the empty string "" means Patroni will use K8s as a DCS - DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spilo-11:1.5-p9"` + DockerImage string `name:"docker_image" default:"registry.opensource.zalan.do/acid/spilo-cdp-12:1.6-p16"` Sidecars map[string]string `name:"sidecar_docker_images"` // default name `operator` enables backward compatibility with the older ServiceAccountName field PodServiceAccountName string `name:"pod_service_account_name" default:"operator"` @@ -109,6 +114,7 @@ type Config struct { EnableMasterLoadBalancer bool `name:"enable_master_load_balancer" default:"true"` EnableReplicaLoadBalancer bool `name:"enable_replica_load_balancer" default:"false"` CustomServiceAnnotations map[string]string `name:"custom_service_annotations"` + CustomPodAnnotations map[string]string `name:"custom_pod_annotations"` EnablePodAntiAffinity bool `name:"enable_pod_antiaffinity" default:"false"` PodAntiAffinityTopologyKey string `name:"pod_antiaffinity_topology_key" default:"kubernetes.io/hostname"` // deprecated and kept for backward compatibility @@ -117,6 +123,8 @@ type Config struct { ReplicaDNSNameFormat StringTemplate `name:"replica_dns_name_format" default:"{cluster}-repl.{team}.{hostedzone}"` PDBNameFormat StringTemplate `name:"pdb_name_format" default:"postgres-{cluster}-pdb"` EnablePodDisruptionBudget *bool `name:"enable_pod_disruption_budget" default:"true"` + EnableInitContainers *bool `name:"enable_init_containers" default:"true"` + EnableSidecars *bool `name:"enable_sidecars" default:"true"` Workers uint32 `name:"workers" default:"4"` APIPort int `name:"api_port" default:"8080"` RingLogLines int `name:"ring_log_lines" default:"100"` diff --git a/pkg/util/constants/kubernetes.go b/pkg/util/constants/kubernetes.go index a4ea73e80..be79687eb 100644 --- a/pkg/util/constants/kubernetes.go +++ b/pkg/util/constants/kubernetes.go @@ -4,11 +4,9 @@ import "time" // General kubernetes-related constants const ( - PostgresContainerName = "postgres" - PostgresContainerIdx = 0 - K8sAPIPath = "/apis" - StatefulsetDeletionInterval = 1 * time.Second - StatefulsetDeletionTimeout = 30 * time.Second + PostgresContainerName = "postgres" + PostgresContainerIdx = 0 + K8sAPIPath = "/apis" QueueResyncPeriodPod = 5 * time.Minute QueueResyncPeriodTPR = 5 * time.Minute diff --git a/pkg/util/k8sutil/k8sutil.go b/pkg/util/k8sutil/k8sutil.go index 66b51dd1f..118d1df53 100644 --- a/pkg/util/k8sutil/k8sutil.go +++ b/pkg/util/k8sutil/k8sutil.go @@ -16,8 +16,8 @@ import ( apiextbeta1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/kubernetes/typed/apps/v1beta1" - v1core "k8s.io/client-go/kubernetes/typed/core/v1" + appsv1 "k8s.io/client-go/kubernetes/typed/apps/v1" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" policyv1beta1 "k8s.io/client-go/kubernetes/typed/policy/v1beta1" rbacv1beta1 "k8s.io/client-go/kubernetes/typed/rbac/v1beta1" "k8s.io/client-go/rest" @@ -29,17 +29,17 @@ import ( // KubernetesClient describes getters for Kubernetes objects type KubernetesClient struct { - v1core.SecretsGetter - v1core.ServicesGetter - v1core.EndpointsGetter - v1core.PodsGetter - v1core.PersistentVolumesGetter - v1core.PersistentVolumeClaimsGetter - v1core.ConfigMapsGetter - v1core.NodesGetter - v1core.NamespacesGetter - v1core.ServiceAccountsGetter - v1beta1.StatefulSetsGetter + corev1.SecretsGetter + corev1.ServicesGetter + corev1.EndpointsGetter + corev1.PodsGetter + corev1.PersistentVolumesGetter + corev1.PersistentVolumeClaimsGetter + corev1.ConfigMapsGetter + corev1.NodesGetter + corev1.NamespacesGetter + corev1.ServiceAccountsGetter + appsv1.StatefulSetsGetter rbacv1beta1.RoleBindingsGetter policyv1beta1.PodDisruptionBudgetsGetter apiextbeta1.CustomResourceDefinitionsGetter @@ -50,14 +50,14 @@ type KubernetesClient struct { } type mockSecret struct { - v1core.SecretInterface + corev1.SecretInterface } type MockSecretGetter struct { } type mockConfigMap struct { - v1core.ConfigMapInterface + corev1.ConfigMapInterface } type MockConfigMapsGetter struct { @@ -82,7 +82,7 @@ func ResourceNotFound(err error) bool { return apierrors.IsNotFound(err) } -// NewFromConfig create Kubernets Interface using REST config +// NewFromConfig create Kubernetes Interface using REST config func NewFromConfig(cfg *rest.Config) (KubernetesClient, error) { kubeClient := KubernetesClient{} @@ -101,7 +101,7 @@ func NewFromConfig(cfg *rest.Config) (KubernetesClient, error) { kubeClient.PersistentVolumesGetter = client.CoreV1() kubeClient.NodesGetter = client.CoreV1() kubeClient.NamespacesGetter = client.CoreV1() - kubeClient.StatefulSetsGetter = client.AppsV1beta1() + kubeClient.StatefulSetsGetter = client.AppsV1() kubeClient.PodDisruptionBudgetsGetter = client.PolicyV1beta1() kubeClient.RESTClient = client.CoreV1().RESTClient() kubeClient.RoleBindingsGetter = client.RbacV1beta1() @@ -215,12 +215,12 @@ func (c *mockConfigMap) Get(name string, options metav1.GetOptions) (*v1.ConfigM } // Secrets to be mocked -func (c *MockSecretGetter) Secrets(namespace string) v1core.SecretInterface { +func (c *MockSecretGetter) Secrets(namespace string) corev1.SecretInterface { return &mockSecret{} } // ConfigMaps to be mocked -func (c *MockConfigMapsGetter) ConfigMaps(namespace string) v1core.ConfigMapInterface { +func (c *MockConfigMapsGetter) ConfigMaps(namespace string) corev1.ConfigMapInterface { return &mockConfigMap{} } diff --git a/pkg/util/patroni/patroni.go b/pkg/util/patroni/patroni.go index 23260f277..bdd96f048 100644 --- a/pkg/util/patroni/patroni.go +++ b/pkg/util/patroni/patroni.go @@ -5,7 +5,9 @@ import ( "encoding/json" "fmt" "io/ioutil" + "net" "net/http" + "strconv" "time" "github.com/sirupsen/logrus" @@ -43,8 +45,19 @@ func New(logger *logrus.Entry) *Patroni { } } -func apiURL(masterPod *v1.Pod) string { - return fmt.Sprintf("http://%s:%d", masterPod.Status.PodIP, apiPort) +func apiURL(masterPod *v1.Pod) (string, error) { + ip := net.ParseIP(masterPod.Status.PodIP) + if ip == nil { + return "", fmt.Errorf("%s is not a valid IP", masterPod.Status.PodIP) + } + // Sanity check PodIP + if ip.To4() == nil { + if ip.To16() == nil { + // Shouldn't ever get here, but library states it's possible. + return "", fmt.Errorf("%s is not a valid IPv4/IPv6 address", masterPod.Status.PodIP) + } + } + return fmt.Sprintf("http://%s", net.JoinHostPort(ip.String(), strconv.Itoa(apiPort))), nil } func (p *Patroni) httpPostOrPatch(method string, url string, body *bytes.Buffer) (err error) { @@ -88,7 +101,11 @@ func (p *Patroni) Switchover(master *v1.Pod, candidate string) error { if err != nil { return fmt.Errorf("could not encode json: %v", err) } - return p.httpPostOrPatch(http.MethodPost, apiURL(master)+failoverPath, buf) + apiURLString, err := apiURL(master) + if err != nil { + return err + } + return p.httpPostOrPatch(http.MethodPost, apiURLString+failoverPath, buf) } //TODO: add an option call /patroni to check if it is necessary to restart the server @@ -100,5 +117,9 @@ func (p *Patroni) SetPostgresParameters(server *v1.Pod, parameters map[string]st if err != nil { return fmt.Errorf("could not encode json: %v", err) } - return p.httpPostOrPatch(http.MethodPatch, apiURL(server)+configPath, buf) + apiURLString, err := apiURL(server) + if err != nil { + return err + } + return p.httpPostOrPatch(http.MethodPatch, apiURLString+configPath, buf) } diff --git a/pkg/util/patroni/patroni_test.go b/pkg/util/patroni/patroni_test.go new file mode 100644 index 000000000..388120ae5 --- /dev/null +++ b/pkg/util/patroni/patroni_test.go @@ -0,0 +1,74 @@ +package patroni + +import ( + "errors" + "fmt" + "k8s.io/api/core/v1" + "testing" +) + +func newMockPod(ip string) *v1.Pod { + return &v1.Pod{ + Status: v1.PodStatus{ + PodIP: ip, + }, + } +} + +func TestApiURL(t *testing.T) { + var testTable = []struct { + podIP string + expectedResponse string + expectedError error + }{ + { + "127.0.0.1", + fmt.Sprintf("http://127.0.0.1:%d", apiPort), + nil, + }, + { + "0000:0000:0000:0000:0000:0000:0000:0001", + fmt.Sprintf("http://[::1]:%d", apiPort), + nil, + }, + { + "::1", + fmt.Sprintf("http://[::1]:%d", apiPort), + nil, + }, + { + "", + "", + errors.New(" is not a valid IP"), + }, + { + "foobar", + "", + errors.New("foobar is not a valid IP"), + }, + { + "127.0.1", + "", + errors.New("127.0.1 is not a valid IP"), + }, + { + ":::", + "", + errors.New("::: is not a valid IP"), + }, + } + for _, test := range testTable { + resp, err := apiURL(newMockPod(test.podIP)) + if resp != test.expectedResponse { + t.Errorf("expected response %v does not match the actual %v", test.expectedResponse, resp) + } + if err != test.expectedError { + if err == nil || test.expectedError == nil { + t.Errorf("expected error '%v' does not match the actual error '%v'", test.expectedError, err) + } + if err != nil && test.expectedError != nil && err.Error() != test.expectedError.Error() { + t.Errorf("expected error '%v' does not match the actual error '%v'", test.expectedError, err) + } + } + } +} diff --git a/pkg/util/teams/teams_test.go b/pkg/util/teams/teams_test.go index 637c4e16c..51bbcbc31 100644 --- a/pkg/util/teams/teams_test.go +++ b/pkg/util/teams/teams_test.go @@ -24,7 +24,7 @@ var teamsAPItc = []struct { {`{ "dn": "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net", "id": "acid", -"id_name": "ACID", +"id_name": "acid", "team_id": "111222", "type": "official", "name": "Acid team name", @@ -70,7 +70,7 @@ var teamsAPItc = []struct { &Team{ Dn: "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net", ID: "acid", - TeamName: "ACID", + TeamName: "acid", TeamID: "111222", Type: "official", FullName: "Acid team name", diff --git a/pkg/util/util.go b/pkg/util/util.go index a8ef460db..ad6de14a2 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -141,17 +141,17 @@ func Coalesce(val, defaultVal string) string { return val } -// RequestIsSmallerThanLimit : ... -func RequestIsSmallerThanLimit(requestStr, limitStr string) (bool, error) { +// IsSmallerQuantity : checks if first resource is of a smaller quantity than the second +func IsSmallerQuantity(requestStr, limitStr string) (bool, error) { request, err := resource.ParseQuantity(requestStr) if err != nil { - return false, fmt.Errorf("could not parse memory request %v : %v", requestStr, err) + return false, fmt.Errorf("could not parse request %v : %v", requestStr, err) } limit, err2 := resource.ParseQuantity(limitStr) if err2 != nil { - return false, fmt.Errorf("could not parse memory limit %v : %v", limitStr, err2) + return false, fmt.Errorf("could not parse limit %v : %v", limitStr, err2) } return request.Cmp(limit) == -1, nil diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index a34e57e23..1f86ea1b4 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -69,7 +69,7 @@ var substringMatch = []struct { {regexp.MustCompile(`aaaa (\d+) bbbb`), "aaaa 123 bbbb", nil}, } -var requestIsSmallerThanLimitTests = []struct { +var requestIsSmallerQuantityTests = []struct { request string limit string out bool @@ -155,14 +155,14 @@ func TestMapContains(t *testing.T) { } } -func TestRequestIsSmallerThanLimit(t *testing.T) { - for _, tt := range requestIsSmallerThanLimitTests { - res, err := RequestIsSmallerThanLimit(tt.request, tt.limit) +func TestIsSmallerQuantity(t *testing.T) { + for _, tt := range requestIsSmallerQuantityTests { + res, err := IsSmallerQuantity(tt.request, tt.limit) if err != nil { - t.Errorf("RequestIsSmallerThanLimit returned unexpected error: %#v", err) + t.Errorf("IsSmallerQuantity returned unexpected error: %#v", err) } if res != tt.out { - t.Errorf("RequestIsSmallerThanLimit expected: %#v, got: %#v", tt.out, res) + t.Errorf("IsSmallerQuantity expected: %#v, got: %#v", tt.out, res) } } } diff --git a/run_operator_locally.sh b/run_operator_locally.sh index f5044dc14..9e3e082da 100755 --- a/run_operator_locally.sh +++ b/run_operator_locally.sh @@ -98,7 +98,7 @@ function build_operator_binary(){ # redirecting stderr greatly reduces non-informative output during normal builds echo "Build operator binary (stderr redirected to /dev/null)..." - make clean tools deps local test > /dev/null 2>&1 + make clean deps local test > /dev/null 2>&1 } diff --git a/ui/operator_ui/spiloutils.py b/ui/operator_ui/spiloutils.py index 5a7ad6868..7f080e3c9 100644 --- a/ui/operator_ui/spiloutils.py +++ b/ui/operator_ui/spiloutils.py @@ -82,7 +82,7 @@ def request_delete(cluster, path, **kwargs): def resource_api_version(resource_type): return { 'postgresqls': 'apis/acid.zalan.do/v1', - 'statefulsets': 'apis/apps/v1beta1', + 'statefulsets': 'apis/apps/v1', }.get(resource_type, 'api/v1')