diff --git a/Dockerfile.agent b/Dockerfile.agent new file mode 100644 index 00000000..1646bbed --- /dev/null +++ b/Dockerfile.agent @@ -0,0 +1,59 @@ +ARG GOLANG_BUILDER=golang:1.16 +ARG OPERATOR_BASE_IMAGE=gcr.io/distroless/base + +# Build the manager binary +FROM $GOLANG_BUILDER AS builder + +#Arguments required by OSBS build system +ARG CACHITO_ENV_FILE=/remote-source/cachito.env + +ARG REMOTE_SOURCE=. +ARG REMOTE_SOURCE_DIR=/remote-source +ARG REMOTE_SOURCE_SUBDIR= +ARG DEST_ROOT=/dest-root +ARG WHAT=osp-director-operator-agent + +ARG GO_BUILD_EXTRA_ARGS= + +COPY $REMOTE_SOURCE $REMOTE_SOURCE_DIR +WORKDIR $REMOTE_SOURCE_DIR/$REMOTE_SOURCE_SUBDIR + +RUN if [ -f $CACHITO_ENV_FILE ] ; then source $CACHITO_ENV_FILE ; fi ; CGO_ENABLED=1 go build ${GO_BUILD_EXTRA_ARGS} -o ${DEST_ROOT}/${WHAT} ./containers/agent + +# Use distroless as minimal base image to package the manager binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM $OPERATOR_BASE_IMAGE + +# Those arguments must match ones from builder +ARG DEST_ROOT=/dest-root +ARG WHAT=osp-director-operator-agent + +ARG IMAGE_COMPONENT="osp-director-operator-agent-container" +ARG IMAGE_NAME="osp-director-agent-container-container" +ARG IMAGE_VERSION="1.0.0" +ARG IMAGE_SUMMARY="OSP-Director-Operator-agent" +ARG IMAGE_DESC="This is agent that works with the OSP Director Operator" +ARG IMAGE_TAGS="openstack director" + +### DO NOT EDIT LINES BELOW +# Auto generated using CI tools from +# https://github.com/openstack-k8s-operators/openstack-k8s-operators-ci + +# Labels required by upstream and osbs build system +LABEL com.redhat.component="${IMAGE_COMPONENT}" \ + name="${IMAGE_NAME}" \ + version="${IMAGE_VERSION}" \ + summary="${IMAGE_SUMMARY}" \ + io.k8s.name="${IMAGE_NAME}" \ + io.k8s.description="${IMAGE_DESC}" \ + io.openshift.tags="${IMAGE_TAGS}" +### DO NOT EDIT LINES ABOVE + +WORKDIR / + +# Install binary to WORKDIR +COPY --from=builder ${DEST_ROOT}/${WHAT} . + +ENV PATH="/:${PATH}" + +ENTRYPOINT ["/osp-director-operator-agent"] diff --git a/PROJECT b/PROJECT index e8869e0f..7f9a00b6 100644 --- a/PROJECT +++ b/PROJECT @@ -155,4 +155,16 @@ resources: webhooks: validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: openstack.org + group: osp-director + kind: OpenStackDeploy + path: github.com/openstack-k8s-operators/osp-director-operator/api/v1beta1 + version: v1beta1 + webhooks: + defaulting: true + webhookVersion: v1 version: "3" diff --git a/api/v1beta1/openstackdeploy_types.go b/api/v1beta1/openstackdeploy_types.go new file mode 100644 index 00000000..072fbc19 --- /dev/null +++ b/api/v1beta1/openstackdeploy_types.go @@ -0,0 +1,109 @@ +/* + + +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. +*/ + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // + // condition types + // + + // DeployCondTypeWaiting - the deployment is waiting + DeployCondTypeWaiting ProvisioningState = "Waiting" + // DeployCondTypeInitializing - the deployment is waiting + DeployCondTypeInitializing ProvisioningState = "Initializing" + // DeployCondTypeRunning - the deployment is running + DeployCondTypeRunning ProvisioningState = "Running" + // DeployCondTypeFinished - the deploy has finished executing + DeployCondTypeFinished ProvisioningState = "Finished" + // DeployCondTypeError - the deployment hit a generic error + DeployCondTypeError ProvisioningState = "Error" + + // DeployCondReasonJobCreated - job created + DeployCondReasonJobCreated ConditionReason = "JobCreated" + // DeployCondReasonJobCreateFailed - job create failed + DeployCondReasonJobCreateFailed ConditionReason = "JobCreated" + // DeployCondReasonJobDelete - job deleted + DeployCondReasonJobDelete ConditionReason = "JobDeleted" + // DeployCondReasonJobFinished - job deleted + DeployCondReasonJobFinished ConditionReason = "JobFinished" + // DeployCondReasonCVUpdated - error creating/update ConfigVersion + DeployCondReasonCVUpdated ConditionReason = "ConfigVersionUpdated" + // DeployCondReasonConfigVersionNotFound - error finding ConfigVersion + DeployCondReasonConfigVersionNotFound ConditionReason = "ConfigVersionNotFound" + // DeployCondReasonJobFailed - error creating/update CM + DeployCondReasonJobFailed ConditionReason = "JobFailed" + // DeployCondReasonConfigCreate - error creating/update CM + DeployCondReasonConfigCreate ConditionReason = "ConfigCreate" +) + +// OpenStackDeploySpec defines the desired state of OpenStackDeploy +type OpenStackDeploySpec struct { + // Name of the image + ImageURL string `json:"imageURL"` + + // ConfigVersion the config version/git hash of the playbooks to deploy. + ConfigVersion string `json:"configVersion,omitempty"` + + // ConfigGenerator name of the configGenerator + ConfigGenerator string `json:"configGenerator"` +} + +// OpenStackDeployStatus defines the observed state of OpenStackDeploy +type OpenStackDeployStatus struct { + // ConfigVersion hash that has been deployed + ConfigVersion string `json:"configVersion"` + + // CurrentState + CurrentState ProvisioningState `json:"currentState"` + + // CurrentReason + CurrentReason ConditionReason `json:"currentReason"` + + Conditions ConditionList `json:"conditions,omitempty" optional:"true"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:resource:shortName=osdeploy;osdeploys;osdepl +//+operator-sdk:csv:customresourcedefinitions:displayName="OpenStack Deploy" +//+kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.currentState`,description="Status" + +// OpenStackDeploy is the Schema for the openstackdeploys API +type OpenStackDeploy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec OpenStackDeploySpec `json:"spec,omitempty"` + Status OpenStackDeployStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// OpenStackDeployList contains a list of OpenStackDeploy +type OpenStackDeployList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []OpenStackDeploy `json:"items"` +} + +func init() { + SchemeBuilder.Register(&OpenStackDeploy{}, &OpenStackDeployList{}) +} diff --git a/api/v1beta1/openstackdeploy_webhook.go b/api/v1beta1/openstackdeploy_webhook.go new file mode 100644 index 00000000..179670fc --- /dev/null +++ b/api/v1beta1/openstackdeploy_webhook.go @@ -0,0 +1,54 @@ +/* + + +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. +*/ + +package v1beta1 + +import ( + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var openstackdeploylog = logf.Log.WithName("openstackdeploy-resource") + +// OpenStackDeployDefaults - +type OpenStackDeployDefaults struct { + AgentImageURL string +} + +var openstackDeployDefaults OpenStackDeployDefaults + +// SetupWebhookWithManager - +func (r *OpenStackDeploy) SetupWebhookWithManager(mgr ctrl.Manager, defaults OpenStackDeployDefaults) error { + openstackDeployDefaults = defaults + + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +//+kubebuilder:webhook:path=/mutate-osp-director-openstack-org-v1beta1-openstackdeploy,mutating=true,failurePolicy=fail,sideEffects=None,groups=osp-director.openstack.org,resources=openstackdeploys,verbs=create;update,versions=v1beta1,name=mopenstackdeploy.kb.io,admissionReviewVersions={v1,v1beta1} + +var _ webhook.Defaulter = &OpenStackDeploy{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *OpenStackDeploy) Default() { + openstackdeploylog.Info("default", "name", r.Name) + if r.Spec.ImageURL == "" { + r.Spec.ImageURL = openstackDeployDefaults.AgentImageURL + } +} diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 213f0f5a..41b6ac39 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -1281,6 +1281,117 @@ func (in *OpenStackControlPlaneStatus) DeepCopy() *OpenStackControlPlaneStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenStackDeploy) DeepCopyInto(out *OpenStackDeploy) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackDeploy. +func (in *OpenStackDeploy) DeepCopy() *OpenStackDeploy { + if in == nil { + return nil + } + out := new(OpenStackDeploy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OpenStackDeploy) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenStackDeployDefaults) DeepCopyInto(out *OpenStackDeployDefaults) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackDeployDefaults. +func (in *OpenStackDeployDefaults) DeepCopy() *OpenStackDeployDefaults { + if in == nil { + return nil + } + out := new(OpenStackDeployDefaults) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenStackDeployList) DeepCopyInto(out *OpenStackDeployList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OpenStackDeploy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackDeployList. +func (in *OpenStackDeployList) DeepCopy() *OpenStackDeployList { + if in == nil { + return nil + } + out := new(OpenStackDeployList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OpenStackDeployList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenStackDeploySpec) DeepCopyInto(out *OpenStackDeploySpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackDeploySpec. +func (in *OpenStackDeploySpec) DeepCopy() *OpenStackDeploySpec { + if in == nil { + return nil + } + out := new(OpenStackDeploySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenStackDeployStatus) DeepCopyInto(out *OpenStackDeployStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(ConditionList, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackDeployStatus. +func (in *OpenStackDeployStatus) DeepCopy() *OpenStackDeployStatus { + if in == nil { + return nil + } + out := new(OpenStackDeployStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OpenStackEphemeralHeat) DeepCopyInto(out *OpenStackEphemeralHeat) { *out = *in diff --git a/config/crd/bases/osp-director.openstack.org_openstackdeploys.yaml b/config/crd/bases/osp-director.openstack.org_openstackdeploys.yaml new file mode 100644 index 00000000..0634a9b0 --- /dev/null +++ b/config/crd/bases/osp-director.openstack.org_openstackdeploys.yaml @@ -0,0 +1,118 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.6.1 + creationTimestamp: null + name: openstackdeploys.osp-director.openstack.org +spec: + group: osp-director.openstack.org + names: + kind: OpenStackDeploy + listKind: OpenStackDeployList + plural: openstackdeploys + shortNames: + - osdeploy + - osdeploys + - osdepl + singular: openstackdeploy + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Status + jsonPath: .status.currentState + name: Status + type: string + name: v1beta1 + schema: + openAPIV3Schema: + description: OpenStackDeploy is the Schema for the openstackdeploys API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: OpenStackDeploySpec defines the desired state of OpenStackDeploy + properties: + configGenerator: + description: ConfigGenerator name of the configGenerator + type: string + configVersion: + description: ConfigVersion the config version/git hash of the playbooks + to deploy. + type: string + imageURL: + description: Name of the image + type: string + required: + - configGenerator + - imageURL + type: object + status: + description: OpenStackDeployStatus defines the observed state of OpenStackDeploy + properties: + conditions: + description: ConditionList - A list of conditions + items: + description: Condition - A particular overall condition of a certain + resource + properties: + lastHearbeatTime: + format: date-time + type: string + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + description: ConditionReason - Why a particular condition is + true, false or unknown + type: string + status: + type: string + type: + description: ConditionType - A summarizing name for a given + condition + type: string + required: + - status + - type + type: object + type: array + configVersion: + description: ConfigVersion hash that has been deployed + type: string + currentReason: + description: CurrentReason + type: string + currentState: + description: CurrentState + type: string + required: + - configVersion + - currentReason + - currentState + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index e095646a..180f319f 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -16,6 +16,7 @@ resources: - bases/osp-director.openstack.org_openstacknetattachments.yaml - bases/osp-director.openstack.org_openstackbackups.yaml - bases/osp-director.openstack.org_openstackbackuprequests.yaml +- bases/osp-director.openstack.org_openstackdeploys.yaml # +kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -36,6 +37,7 @@ patchesStrategicMerge: #- patches/webhook_in_openstacknetattachments.yaml #- patches/webhook_in_openstackbackups.yaml #- patches/webhook_in_openstackbackuprequests.yaml +#- patches/webhook_in_openstackdeploys.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. @@ -55,6 +57,7 @@ patchesStrategicMerge: - patches/cainjection_in_openstacknetattachments.yaml #- patches/cainjection_in_openstackbackups.yaml #- patches/cainjection_in_openstackbackuprequests.yaml +- patches/cainjection_in_openstackdeploys.yaml # +kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_openstackdeploys.yaml b/config/crd/patches/cainjection_in_openstackdeploys.yaml new file mode 100644 index 00000000..fad86fbb --- /dev/null +++ b/config/crd/patches/cainjection_in_openstackdeploys.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: openstackdeploys.osp-director.openstack.org diff --git a/config/crd/patches/webhook_in_openstackdeploys.yaml b/config/crd/patches/webhook_in_openstackdeploys.yaml new file mode 100644 index 00000000..94b42fcb --- /dev/null +++ b/config/crd/patches/webhook_in_openstackdeploys.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: openstackdeploys.osp-director.openstack.org +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/default/manager_default_images.yaml b/config/default/manager_default_images.yaml index 1801ec12..795439a7 100644 --- a/config/default/manager_default_images.yaml +++ b/config/default/manager_default_images.yaml @@ -25,6 +25,8 @@ spec: value: registry.redhat.io/rhosp-rhel8-tech-preview/osp-director-downloader:1.0 - name: PROVISIONING_AGENT_IMAGE_URL_DEFAULT value: registry.redhat.io/rhosp-rhel8-tech-preview/osp-director-provisioner:1.0 + - name: AGENT_IMAGE_URL_DEFAULT + value: quay.io/openstack-k8s-operators/osp-director-operator-agent:0.0.1 - name: APACHE_IMAGE_URL_DEFAULT value: registry.redhat.io/rhel8/httpd-24:latest - name: OPENSTACK_RELEASE_DEFAULT diff --git a/config/manifests/bases/osp-director-operator.clusterserviceversion.yaml b/config/manifests/bases/osp-director-operator.clusterserviceversion.yaml index d649fbdc..ebc23983 100644 --- a/config/manifests/bases/osp-director-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/osp-director-operator.clusterserviceversion.yaml @@ -70,6 +70,11 @@ spec: kind: OpenStackControlPlane name: openstackcontrolplanes.osp-director.openstack.org version: v1beta1 + - description: OpenStackDeploy is the Schema for the openstackdeploys API + displayName: OpenStack Deploy + kind: OpenStackDeploy + name: openstackdeploys.osp-director.openstack.org + version: v1beta1 - description: OpenStackEphemeralHeat is the Schema for the openstackephemeralheats API displayName: OpenStack Ephemeral Heat @@ -153,6 +158,8 @@ spec: name: osp-director-operator-downloader - image: registry.redhat.io/rhosp-rhel8-tech-preview/osp-director-provisioner:1.0 name: osp-director-operator-provisioner + - image: quay.io/openstack-k8s-operators/osp-director-operator-agent:0.0.1 + name: osp-director-operator-agent - image: controller:latest name: osp-director-operator - image: registry.redhat.io/rhel8/httpd-24:latest diff --git a/config/rbac/openstackdeploy_editor_role.yaml b/config/rbac/openstackdeploy_editor_role.yaml new file mode 100644 index 00000000..8f7f31e2 --- /dev/null +++ b/config/rbac/openstackdeploy_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit openstackdeploys. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: openstackdeploy-editor-role +rules: +- apiGroups: + - osp-director.openstack.org + resources: + - openstackdeploys + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - osp-director.openstack.org + resources: + - openstackdeploys/status + verbs: + - get diff --git a/config/rbac/openstackdeploy_viewer_role.yaml b/config/rbac/openstackdeploy_viewer_role.yaml new file mode 100644 index 00000000..7d13ed31 --- /dev/null +++ b/config/rbac/openstackdeploy_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view openstackdeploys. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: openstackdeploy-viewer-role +rules: +- apiGroups: + - osp-director.openstack.org + resources: + - openstackdeploys + verbs: + - get + - list + - watch +- apiGroups: + - osp-director.openstack.org + resources: + - openstackdeploys/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 2a550cd9..c03fd0fb 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -427,6 +427,32 @@ rules: - get - patch - update +- apiGroups: + - osp-director.openstack.org + resources: + - openstackdeploys + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - osp-director.openstack.org + resources: + - openstackdeploys/finalizers + verbs: + - update +- apiGroups: + - osp-director.openstack.org + resources: + - openstackdeploys/status + verbs: + - get + - patch + - update - apiGroups: - osp-director.openstack.org resources: diff --git a/config/rbac/service_account.yaml b/config/rbac/service_account.yaml index f0750012..5bf7af94 100644 --- a/config/rbac/service_account.yaml +++ b/config/rbac/service_account.yaml @@ -87,6 +87,70 @@ rules: - list - update --- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: openstackdeploy +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: openstackdeploy-role + namespace: openstack +rules: +- apiGroups: + - "" + resources: + - pods/exec + verbs: + - create + - post +- apiGroups: + - "" + resources: + - pods + verbs: + - get + - list +- apiGroups: + - "" + resources: + - pods + verbs: + - create + - get + - list + - patch + - post + - update + - watch +- apiGroups: + - security.openshift.io + resourceNames: + - anyuid + resources: + - securitycontextconstraints + verbs: + - use +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: openstackdeploy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: openstackdeploy-role +subjects: + # Applying the role to both SA (with and without prefix) to be able + # to run the operator local +- kind: ServiceAccount + name: osp-director-operator-openstackdeploy + namespace: openstack +- kind: ServiceAccount + name: openstackdeploy + namespace: openstack +--- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index d20732e6..2cfb3133 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -9,6 +9,7 @@ resources: - osp-director_v1beta1_openstackconfiggenerator.yaml - osp-director_v1beta1_openstacknetconfig.yaml - osp-director_v1beta1_openstacknetattachment.yaml +- osp-director_v1beta1_openstackdeploy.yaml # +kubebuilder:scaffold:manifestskustomizesamples # These patches will "correct" some values for samples injected into the CSV, # but leave the samples as-is for functional testing purposes diff --git a/config/samples/osp-director_v1beta1_openstackdeploy.yaml b/config/samples/osp-director_v1beta1_openstackdeploy.yaml new file mode 100644 index 00000000..ffb1902d --- /dev/null +++ b/config/samples/osp-director_v1beta1_openstackdeploy.yaml @@ -0,0 +1,7 @@ +apiVersion: osp-director.openstack.org/v1beta1 +kind: OpenStackDeploy +metadata: + name: openstackdeploy-sample +spec: + configVersion: bar + configGenerator: foo diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 20b1c229..a7ad3204 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -48,6 +48,27 @@ webhooks: resources: - openstackcontrolplanes sideEffects: None +- admissionReviewVersions: + - v1 + - v1beta1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-osp-director-openstack-org-v1beta1-openstackdeploy + failurePolicy: Fail + name: mopenstackdeploy.kb.io + rules: + - apiGroups: + - osp-director.openstack.org + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - openstackdeploys + sideEffects: None - admissionReviewVersions: - v1 - v1beta1 diff --git a/containers/agent/build.sh b/containers/agent/build.sh new file mode 100755 index 00000000..b32e8f3e --- /dev/null +++ b/containers/agent/build.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +set -eux + +REPO=github.com/openstack-k8s-operators/osp-director-operator +WHAT=agent +GOFLAGS=${GOFLAGS:-} +GLDFLAGS=${GLDFLAGS:-} + +GOOS=$(go env GOOS) +GOARCH=$(go env GOARCH) + +# Go to the root of the repo +cdup="$(git rev-parse --show-cdup)" && test -n "$cdup" && cd "$cdup" + +if [ -z ${VERSION_OVERRIDE+a} ]; then + echo "Using version from git..." + VERSION_OVERRIDE=$(git describe --abbrev=8 --dirty --always) +fi + +GLDFLAGS+="-X ${REPO}/pkg/version.Raw=${VERSION_OVERRIDE}" + +if [ -z ${BIN_PATH+a} ]; then + export BIN_PATH=build/_output/${GOOS}/${GOARCH} +fi + +mkdir -p ${BIN_PATH} + +CGO_ENABLED=1 + +echo "Building ${REPO}/cmd/${WHAT}" +go build -o ${BIN_PATH}/${WHAT} ./containers/agent diff --git a/containers/agent/deploy.go b/containers/agent/deploy.go new file mode 100644 index 00000000..8ab461a2 --- /dev/null +++ b/containers/agent/deploy.go @@ -0,0 +1,166 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io" + "os" + "strings" + + "github.com/golang/glog" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/remotecommand" + "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +var ( + deployCmd = &cobra.Command{ + Use: "deploy", + Short: "Execute an OpenStack Deploy workflow", + Long: "", + Run: runDeployCmd, + } + + deployOpts struct { + kubeconfig string + namespace string + pod string + configVersion string + gitURL string + gitSSHIdentity string + } +) + +func init() { + rootCmd.AddCommand(deployCmd) + deployCmd.PersistentFlags().StringVar(&deployOpts.namespace, "namespace", "openstack", "Namespace to use for openstackclient pod.") + deployCmd.PersistentFlags().StringVar(&deployOpts.pod, "pod", "", "Pod to use for executing the deployment.") + deployCmd.PersistentFlags().StringVar(&deployOpts.configVersion, "configVersion", "", "Config version to use when executing the deployment.") + deployCmd.PersistentFlags().StringVar(&deployOpts.gitURL, "gitURL", "", "Git URL to use when downloading playbooks.") + deployCmd.PersistentFlags().StringVar(&deployOpts.gitSSHIdentity, "gitSSHIdentity", "", "Git SSH Identity to use when downloading playbooks.") +} + +// ExecPodCommand - +func ExecPodCommand(kclient kubernetes.Clientset, pod corev1.Pod, containerName string, command string) error { + req := kclient.CoreV1().RESTClient().Post(). + Namespace(pod.Namespace). + Resource("pods"). + Name(pod.Name). + SubResource("exec"). + Param("container", containerName). + Param("stdin", "true"). + Param("stdout", "true"). + Param("stderr", "true"). + Param("tty", "false"). + Param("command", "sh") + + cfg, err := config.GetConfig() + + if err != nil { + return err + } + + exec, err := remotecommand.NewSPDYExecutor(cfg, "POST", req.URL()) + if err != nil { + return err + } + + argString := fmt.Sprintf("%s\n", command) + reader := strings.NewReader(argString) + + return exec.Stream(remotecommand.StreamOptions{ + Stdin: io.Reader(reader), + Stdout: os.Stdout, + Stderr: os.Stderr, + Tty: false, + }) +} + +func runDeployCmd(cmd *cobra.Command, args []string) { + var err error + err = flag.Set("logtostderr", "true") + if err != nil { + panic(err.Error()) + } + + flag.Parse() + + glog.V(0).Info("Running deploy command.") + + if deployOpts.namespace == "" { + name, ok := os.LookupEnv("OSP_DIRECTOR_OPERATOR_NAMESPACE") + if !ok || name == "" { + glog.Fatalf("namespace is required") + } + deployOpts.namespace = name + } + if deployOpts.pod == "" { + pod, ok := os.LookupEnv("OPENSTACKCLIENT_POD") + if !ok || pod == "" { + glog.Fatalf("pod is required") + } + deployOpts.pod = pod + } + if deployOpts.configVersion == "" { + configVersion, ok := os.LookupEnv("CONFIG_VERSION") + if !ok || configVersion == "" { + glog.Fatalf("configVersion is required") + } + deployOpts.configVersion = configVersion + } + + if deployOpts.gitURL == "" { + gitURL, ok := os.LookupEnv("GIT_URL") + if !ok || gitURL == "" { + glog.Fatalf("gitURL is required") + } + deployOpts.gitURL = gitURL + } + + if deployOpts.gitSSHIdentity == "" { + gitSSHIdentity, ok := os.LookupEnv("GIT_ID_RSA") + if !ok || gitSSHIdentity == "" { + glog.Fatalf("gitSSHIdentity is required") + } + deployOpts.gitSSHIdentity = gitSSHIdentity + } + + var config *rest.Config + kubeconfig := os.Getenv("KUBECONFIG") + if kubeconfig != "" { + config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) + } else { + config, err = rest.InClusterConfig() + } + + if err != nil { + panic(err.Error()) + } + + kclient, err := kubernetes.NewForConfig(config) + if err != nil { + panic(err.Error()) + } + + pod, err := kclient.CoreV1().Pods(deployOpts.namespace).Get( + context.TODO(), + deployOpts.pod, + metav1.GetOptions{}) + + if err != nil { + panic(err.Error()) + } + + execErr := ExecPodCommand(*kclient, *pod, "openstackclient", "CONFIG_VERSION="+deployOpts.configVersion+" GIT_ID_RSA='"+deployOpts.gitSSHIdentity+"' GIT_URL='"+deployOpts.gitURL+"' "+"/usr/bin/bash /usr/local/bin/tripleo-deploy.sh") + if execErr != nil { + panic(execErr.Error()) + } + + glog.V(0).Info("Shutting down deploy agent") +} diff --git a/containers/agent/main.go b/containers/agent/main.go new file mode 100644 index 00000000..d96dfe5b --- /dev/null +++ b/containers/agent/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "flag" + + "github.com/golang/glog" + "github.com/spf13/cobra" +) + +const ( + componentName = "osp-director-operator-agent" +) + +var ( + rootCmd = &cobra.Command{ + Use: componentName, + Short: "Run OSP-Director-Operator Agent", + Long: "Runs the OSP-Director-Operator Agent", + } +) + +func init() { + rootCmd.PersistentFlags().AddGoFlagSet(flag.CommandLine) +} + +func main() { + if err := rootCmd.Execute(); err != nil { + glog.Exitf("Error executing mcd: %v", err) + } +} diff --git a/containers/agent/version.go b/containers/agent/version.go new file mode 100644 index 00000000..48cb9830 --- /dev/null +++ b/containers/agent/version.go @@ -0,0 +1,34 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/spf13/cobra" +) + +var ( + versionCmd = &cobra.Command{ + Use: "version", + Short: "Print the version number of OSP-Director-Operator Agent", + Long: `All software has versions. This is that of the OSP-Director-Operator Agent.`, + Run: runVersionCmd, + } +) + +func init() { + rootCmd.AddCommand(versionCmd) +} + +func runVersionCmd(cmd *cobra.Command, args []string) { + err := flag.Set("logtostderr", "true") + if err != nil { + panic(err.Error()) + } + flag.Parse() + + program := "OSPDirectorOperatorAgent" + version := "v1.0.0" + + fmt.Println(program, version) +} diff --git a/controllers/openstackdeploy_controller.go b/controllers/openstackdeploy_controller.go new file mode 100644 index 00000000..532750fb --- /dev/null +++ b/controllers/openstackdeploy_controller.go @@ -0,0 +1,248 @@ +/* + + +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. +*/ + +package controllers + +import ( + "context" + "fmt" + "time" + + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + k8s_errors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/go-logr/logr" + ospdirectorv1beta1 "github.com/openstack-k8s-operators/osp-director-operator/api/v1beta1" + common "github.com/openstack-k8s-operators/osp-director-operator/pkg/common" + "github.com/openstack-k8s-operators/osp-director-operator/pkg/openstackdeploy" +) + +// OpenStackDeployReconciler reconciles a OpenStackDeploy object +type OpenStackDeployReconciler struct { + client.Client + Kclient kubernetes.Interface + Log logr.Logger + Scheme *runtime.Scheme +} + +// GetClient - +func (r *OpenStackDeployReconciler) GetClient() client.Client { + return r.Client +} + +// GetKClient - +func (r *OpenStackDeployReconciler) GetKClient() kubernetes.Interface { + return r.Kclient +} + +// GetLogger - +func (r *OpenStackDeployReconciler) GetLogger() logr.Logger { + return r.Log +} + +// GetScheme - +func (r *OpenStackDeployReconciler) GetScheme() *runtime.Scheme { + return r.Scheme +} + +// +kubebuilder:rbac:groups=osp-director.openstack.org,resources=openstackdeploys,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=osp-director.openstack.org,resources=openstackdeploys/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=osp-director.openstack.org,resources=openstackdeploys/finalizers,verbs=update +// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;create;update;delete;watch + +// Reconcile - OpenStackDeploy +func (r *OpenStackDeployReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + // Fetch the ProvisionServer instance + instance := &ospdirectorv1beta1.OpenStackDeploy{} + err := r.Client.Get(context.TODO(), req.NamespacedName, instance) + if err != nil { + if k8s_errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. + // For additional cleanup logic use finalizers. Return and don't requeue. + return ctrl.Result{}, nil + } + // Error reading the object - requeue the request. + return ctrl.Result{}, err + } + + // + // initialize condition + // + cond := &ospdirectorv1beta1.Condition{} + + // + // Used in comparisons below to determine whether a status update is actually needed + // + currentStatus := instance.Status.DeepCopy() + statusChanged := func() bool { + return !equality.Semantic.DeepEqual( + r.getNormalizedStatus(&instance.Status), + r.getNormalizedStatus(currentStatus), + ) + } + + defer func(cond *ospdirectorv1beta1.Condition) { + // + // Update object conditions + // + instance.Status.CurrentState = ospdirectorv1beta1.ProvisioningState(cond.Type) + instance.Status.CurrentReason = ospdirectorv1beta1.ConditionReason(cond.Message) + + instance.Status.Conditions.UpdateCurrentCondition( + cond.Type, + cond.Reason, + cond.Message, + ) + + if statusChanged() { + if updateErr := r.Client.Status().Update(context.Background(), instance); updateErr != nil { + if err == nil { + err = common.WrapErrorForObject("Update Status", instance, updateErr) + } else { + common.LogErrorForObject(r, updateErr, "Update status", instance) + } + } + } + + }(cond) + + // look up the git secret from the configGenerator + configGenerator := &ospdirectorv1beta1.OpenStackConfigGenerator{} + err = r.GetClient().Get(context.TODO(), types.NamespacedName{Name: instance.Spec.ConfigGenerator, Namespace: instance.Namespace}, configGenerator) + if err != nil && errors.IsNotFound(err) { + cond.Message = err.Error() + cond.Reason = ospdirectorv1beta1.ConditionReason(ospdirectorv1beta1.DeployCondReasonConfigVersionNotFound) + cond.Type = ospdirectorv1beta1.ConditionType(ospdirectorv1beta1.DeployCondTypeError) + err = common.WrapErrorForObject(cond.Message, instance, err) + + return ctrl.Result{}, err + } + + if instance.Status.ConfigVersion != instance.Spec.ConfigVersion { + // Define a new Job object + job := openstackdeploy.DeployJob(instance, "openstackclient", instance.Spec.ConfigVersion, configGenerator.Spec.GitSecret) + + op, err := controllerutil.CreateOrUpdate(context.TODO(), r.Client, job, func() error { + err := controllerutil.SetControllerReference(instance, job, r.Scheme) + if err != nil { + + cond.Message = err.Error() + cond.Reason = ospdirectorv1beta1.ConditionReason(ospdirectorv1beta1.DeployCondReasonJobCreated) + cond.Type = ospdirectorv1beta1.ConditionType(ospdirectorv1beta1.DeployCondTypeError) + err = common.WrapErrorForObject(cond.Message, instance, err) + + return err + + } + + return nil + }) + if err != nil && !k8s_errors.IsNotFound(err) { + return ctrl.Result{}, err + } + if op != controllerutil.OperationResultNone { + cond.Message = fmt.Sprintf("Job successfully created/updated - operation: %s", string(op)) + cond.Reason = ospdirectorv1beta1.ConditionReason(ospdirectorv1beta1.DeployCondReasonJobCreated) + cond.Type = ospdirectorv1beta1.ConditionType(ospdirectorv1beta1.DeployCondTypeInitializing) + common.LogForObject(r, cond.Message, instance) + + return ctrl.Result{RequeueAfter: time.Second * 5}, nil + } + + // configVersion Hash changed while job was running (NOTE: configVersion is only ENV in the job) + common.LogForObject(r, fmt.Sprintf("Job Hash : %s", job.Spec.Template.Spec.Containers[0].Env[0].Value), instance) + if instance.Spec.ConfigVersion != job.Spec.Template.Spec.Containers[0].Env[0].Value { + _, err = common.DeleteJob(job, r.Kclient, r.Log) + if err != nil { + cond.Message = err.Error() + cond.Reason = ospdirectorv1beta1.ConditionReason(ospdirectorv1beta1.DeployCondReasonJobDelete) + cond.Type = ospdirectorv1beta1.ConditionType(ospdirectorv1beta1.DeployCondTypeError) + err = common.WrapErrorForObject(cond.Message, instance, err) + + return ctrl.Result{}, err + } + + cond.Message = "ConfigVersion has changed. Requeing to start again..." + cond.Reason = ospdirectorv1beta1.ConditionReason(ospdirectorv1beta1.DeployCondReasonCVUpdated) + cond.Type = ospdirectorv1beta1.ConditionType(ospdirectorv1beta1.DeployCondTypeInitializing) + common.LogForObject(r, cond.Message, instance) + + return ctrl.Result{RequeueAfter: time.Second * 5}, nil + + } + + requeue, err := common.WaitOnJob(job, r.Client, r.Log) + common.LogForObject(r, "Deploying OpenStack...", instance) + + if err != nil { + // the job failed in error + cond.Message = "Job failed... Please check job/pod logs." + cond.Reason = ospdirectorv1beta1.ConditionReason(ospdirectorv1beta1.DeployCondReasonJobFailed) + cond.Type = ospdirectorv1beta1.ConditionType(ospdirectorv1beta1.DeployCondTypeError) + err = common.WrapErrorForObject(cond.Message, instance, err) + + return ctrl.Result{}, err + } else if requeue { + cond.Message = "Waiting on OpenStack Deployment..." + cond.Reason = ospdirectorv1beta1.ConditionReason(ospdirectorv1beta1.DeployCondReasonConfigCreate) + cond.Type = ospdirectorv1beta1.ConditionType(ospdirectorv1beta1.DeployCondTypeRunning) + common.LogForObject(r, cond.Message, instance) + + return ctrl.Result{RequeueAfter: time.Second * 10}, nil + } + } + + cond.Message = "The OpenStackDeploy Finished." + cond.Reason = ospdirectorv1beta1.ConditionReason(ospdirectorv1beta1.DeployCondReasonJobFinished) + cond.Type = ospdirectorv1beta1.ConditionType(ospdirectorv1beta1.DeployCondTypeFinished) + common.LogForObject(r, cond.Message, instance) + + return ctrl.Result{}, nil +} + +func (r *OpenStackDeployReconciler) getNormalizedStatus(status *ospdirectorv1beta1.OpenStackDeployStatus) *ospdirectorv1beta1.OpenStackDeployStatus { + + // + // set LastHeartbeatTime and LastTransitionTime to a default value as those + // need to be ignored to compare if conditions changed. + // + s := status.DeepCopy() + for idx := range s.Conditions { + s.Conditions[idx].LastHeartbeatTime = metav1.Time{} + s.Conditions[idx].LastTransitionTime = metav1.Time{} + } + + return s +} + +// SetupWithManager sets up the controller with the Manager. +func (r *OpenStackDeployReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&ospdirectorv1beta1.OpenStackDeploy{}). + Complete(r) +} diff --git a/go.sum b/go.sum index 46856352..6989d20e 100644 --- a/go.sum +++ b/go.sum @@ -368,6 +368,7 @@ github.com/elastic/go-sysinfo v1.1.1/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6 github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU= github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1 h1:yY9rWGoXv1U5pl4gxqlULARMQD7x0QG85lqEXTWysik= github.com/elazarl/goproxy v0.0.0-20190911111923-ecfe977594f1/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/elazarl/goproxy/ext v0.0.0-20190911111923-ecfe977594f1/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= @@ -977,6 +978,7 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= diff --git a/main.go b/main.go index 79f6a073..3a0cb824 100644 --- a/main.go +++ b/main.go @@ -279,6 +279,16 @@ func main() { os.Exit(1) } + if err = (&controllers.OpenStackDeployReconciler{ + Client: mgr.GetClient(), + Kclient: kclient, + Log: ctrl.Log.WithName("controllers").WithName("OpenStackDeployReconciler"), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "OpenStackDeploy") + os.Exit(1) + } + if enableWebhooks { // // DEFAULTS @@ -300,6 +310,9 @@ func main() { ProvisioningAgentImageURL: os.Getenv("PROVISIONING_AGENT_IMAGE_URL_DEFAULT"), ApacheImageURL: os.Getenv("APACHE_IMAGE_URL_DEFAULT"), } + openstackDeployDefaults := ospdirectorv1beta1.OpenStackDeployDefaults{ + AgentImageURL: os.Getenv("AGENT_IMAGE_URL_DEFAULT"), + } // // Register webhooks @@ -360,6 +373,11 @@ func main() { setupLog.Error(err, "unable to create webhook", "webhook", "OpenStackBackupRequest") os.Exit(1) } + + if err = (&ospdirectorv1beta1.OpenStackDeploy{}).SetupWebhookWithManager(mgr, openstackDeployDefaults); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "OpenStackDeploy") + os.Exit(1) + } } // +kubebuilder:scaffold:builder diff --git a/pkg/openstackclient/volumes.go b/pkg/openstackclient/volumes.go index 2e291f6b..5faeceec 100644 --- a/pkg/openstackclient/volumes.go +++ b/pkg/openstackclient/volumes.go @@ -46,7 +46,7 @@ func GetVolumeMounts(instance *ospdirectorv1beta1.OpenStackClient) []corev1.Volu }, { Name: "openstackclient-scripts", - MountPath: "/home/cloud-admin/tripleo-deploy.sh", + MountPath: "/usr/local/bin/tripleo-deploy.sh", SubPath: "tripleo-deploy.sh", ReadOnly: true, }, diff --git a/pkg/openstackconfigversion/git_util.go b/pkg/openstackconfigversion/git_util.go index 1c7e3fb8..63a535d7 100644 --- a/pkg/openstackconfigversion/git_util.go +++ b/pkg/openstackconfigversion/git_util.go @@ -158,9 +158,10 @@ func SyncGit(inst *ospdirectorv1beta1.OpenStackConfigGenerator, client client.Cl return nil, err } + m1 := regexp.MustCompile(`/`) for _, ref := range refs { if ref.Name().IsBranch() { - if ref.Name() == "refs/heads/master" { + if ref.Name() == "refs/heads/master" || ref.Name() == "HEAD" { continue } commit, err := repo.CommitObject(ref.Hash()) @@ -198,17 +199,17 @@ func SyncGit(inst *ospdirectorv1beta1.OpenStackConfigGenerator, client client.Cl //fmt.Println(buffer.String()) configVersion = ospdirectorv1beta1.OpenStackConfigVersion{ ObjectMeta: metav1.ObjectMeta{ - Name: ref.Hash().String(), + Name: m1.Split(ref.Name().String(), -1)[2], Namespace: inst.Namespace, }, - Spec: ospdirectorv1beta1.OpenStackConfigVersionSpec{Hash: ref.Hash().String(), Diff: buffer.String()}} + Spec: ospdirectorv1beta1.OpenStackConfigVersionSpec{Hash: m1.Split(ref.Name().String(), -1)[2], Diff: buffer.String()}} } else { configVersion = ospdirectorv1beta1.OpenStackConfigVersion{ ObjectMeta: metav1.ObjectMeta{ - Name: ref.Hash().String(), + Name: m1.Split(ref.Name().String(), -1)[2], Namespace: inst.Namespace, }, - Spec: ospdirectorv1beta1.OpenStackConfigVersionSpec{Hash: ref.Hash().String(), Diff: ""}} + Spec: ospdirectorv1beta1.OpenStackConfigVersionSpec{Hash: m1.Split(ref.Name().String(), -1)[2], Diff: ""}} } configVersions[ref.Hash().String()] = configVersion } diff --git a/pkg/openstackdeploy/const.go b/pkg/openstackdeploy/const.go new file mode 100644 index 00000000..2d21e6b4 --- /dev/null +++ b/pkg/openstackdeploy/const.go @@ -0,0 +1,22 @@ +/* +Copyright 2021 Red Hat + +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. +*/ + +package openstackdeploy + +const ( + // ServiceAccount - + ServiceAccount = "osp-director-operator-openstackdeploy" +) diff --git a/pkg/openstackdeploy/job.go b/pkg/openstackdeploy/job.go new file mode 100644 index 00000000..5a4fa09d --- /dev/null +++ b/pkg/openstackdeploy/job.go @@ -0,0 +1,103 @@ +/* +Copyright 2021 Red Hat + +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. +*/ + +package openstackdeploy + +import ( + ospdirectorv1beta1 "github.com/openstack-k8s-operators/osp-director-operator/api/v1beta1" + openstackclient "github.com/openstack-k8s-operators/osp-director-operator/pkg/openstackclient" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// DeployJob - +func DeployJob(cr *ospdirectorv1beta1.OpenStackDeploy, openstackClientPod string, configVersion string, gitSecret string) *batchv1.Job { + + runAsUser := int64(openstackclient.CloudAdminUID) + runAsGroup := int64(openstackclient.CloudAdminGID) + + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "deploy-openstack-" + cr.Name, + Namespace: cr.Namespace, + }, + } + + var terminationGracePeriodSeconds int64 = 0 + var backoffLimit int32 = 0 + + cmd := []string{"/osp-director-operator-agent", "deploy"} + restartPolicy := corev1.RestartPolicyNever + + job.Spec.BackoffLimit = &backoffLimit + job.Spec.Template.Spec = corev1.PodSpec{ + RestartPolicy: restartPolicy, + ServiceAccountName: ServiceAccount, + SecurityContext: &corev1.PodSecurityContext{ + RunAsUser: &runAsUser, + RunAsGroup: &runAsGroup, + FSGroup: &runAsGroup, + }, + TerminationGracePeriodSeconds: &terminationGracePeriodSeconds, + Containers: []corev1.Container{ + { + Name: "deploy-openstack", + Image: cr.Spec.ImageURL, + ImagePullPolicy: corev1.PullAlways, + Command: cmd, + Env: []corev1.EnvVar{ + { + Name: "CONFIG_VERSION", + Value: configVersion, + }, + { + Name: "OSP_DIRECTOR_OPERATOR_NAMESPACE", + Value: cr.Namespace, + }, + { + Name: "OPENSTACKCLIENT_POD", + Value: openstackClientPod, + }, + { + Name: "GIT_URL", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: gitSecret, + }, + Key: "git_url", + }, + }, + }, + { + Name: "GIT_ID_RSA", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: gitSecret, + }, + Key: "git_ssh_identity", + }, + }, + }, + }, + }, + }, + } + + return job +} diff --git a/templates/openstackclient/bin/tripleo-deploy.sh b/templates/openstackclient/bin/tripleo-deploy.sh index 560542b5..d1934a3f 100755 --- a/templates/openstackclient/bin/tripleo-deploy.sh +++ b/templates/openstackclient/bin/tripleo-deploy.sh @@ -1,11 +1,28 @@ #!/usr/bin/env bash set -eu +umask 022 -mkdir -p /home/cloud-admin/tripleo-deploy/validations +export PWD=/home/cloud-admin +CONFIG_VERSION=${CONFIG_VERSION:?"Please set CONFIG_VERSION."} +WORKDIR="/home/cloud-admin/work/$CONFIG_VERSION" +mkdir -p $WORKDIR + +# FIXME can this be shared +mkdir -p ~/tripleo-deploy/validations if [ ! -L /var/log/validations ]; then - sudo ln -s /home/cloud-admin/tripleo-deploy/validations /var/log/validations + sudo ln -s ~/tripleo-deploy/validations /var/log/validations fi +GIT_HOST=$(echo $GIT_URL | sed -e 's|^git@\(.*\):.*|\1|g') +GIT_USER=$(echo $GIT_URL | sed -e 's|^git@.*:\(.*\)/.*|\1|g') + +export GIT_SSH_COMMAND="ssh -i $WORKDIR/git_id_rsa -l git -o StrictHostKeyChecking=no" +echo $GIT_ID_RSA | sed -e 's|- |-\n|' | sed -e 's| -|\n-|' > $WORKDIR/git_id_rsa +chmod 600 $WORKDIR/git_id_rsa + +git config --global user.email "dev@null.io" +git config --global user.name "OSP Director Operator" + set_env() { echo -e "Exporting environment variables" export ANSIBLE_SSH_ARGS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o ControlMaster=auto -o ControlPersist=30m -o ServerAliveInterval=64 -o ServerAliveCountMax=1024 -o Compression=no -o TCPKeepAlive=yes -o VerifyHostKeyDNS=no -o ForwardX11=no -o ForwardAgent=yes -o PreferredAuthentications=publickey -T" @@ -27,14 +44,14 @@ set_env() { export ANSIBLE_VARS_PLUGIN_STAGE="inventory" export ANSIBLE_GATHER_SUBSET="!all,min" export ANSIBLE_GATHERING="smart" - export ANSIBLE_LOG_PATH="/home/cloud-admin/ansible.log" + export ANSIBLE_LOG_PATH="$WORKDIR/ansible.log" export ANSIBLE_PRIVATE_KEY_FILE="/home/cloud-admin/.ssh/id_rsa" export ANSIBLE_BECOME="True" export ANSIBLE_LIBRARY="/usr/share/ansible/tripleo-plugins/modules:/usr/share/ansible/plugins/modules:/usr/share/ceph-ansible/library:/usr/share/ansible-modules:/usr/share/ansible/library" export ANSIBLE_LOOKUP_PLUGINS="/usr/share/ansible/tripleo-plugins/lookup:/usr/share/ansible/plugins/lookup:/usr/share/ceph-ansible/plugins/lookup:/usr/share/ansible/lookup_plugins" export ANSIBLE_CALLBACK_PLUGINS="/usr/share/ansible/tripleo-plugins/callback:/usr/share/ansible/plugins/callback:/usr/share/ceph-ansible/plugins/callback:/usr/share/ansible/callback_plugins" export ANSIBLE_ACTION_PLUGINS="/usr/share/ansible/tripleo-plugins/action:/usr/share/ansible/plugins/action:/usr/share/ceph-ansible/plugins/actions:/usr/share/ansible/action_plugins" - export ANSIBLE_FILTER_PLUGINS="/home/cloud-admin/filter:/usr/share/ansible/tripleo-plugins/filter:/usr/share/ansible/plugins/filter:/usr/share/ceph-ansible/plugins/filter:/usr/share/ansible/filter_plugins" + export ANSIBLE_FILTER_PLUGINS="$WORKDIR/filter:/usr/share/ansible/tripleo-plugins/filter:/usr/share/ansible/plugins/filter:/usr/share/ceph-ansible/plugins/filter:/usr/share/ansible/filter_plugins" export ANSIBLE_ROLES_PATH="/usr/share/ansible/tripleo-roles:/usr/share/ansible/roles:/usr/share/ceph-ansible/roles:/etc/ansible/roles:/usr/share/ansible/roles" export LANG="en_US.UTF-8" export HISTCONTROL="ignoredups" @@ -42,41 +59,20 @@ set_env() { } init() { - if [ ! -d /home/cloud-admin/playbooks ]; then - git clone $GIT_URL /home/cloud-admin/playbooks + if [ ! -d $WORKDIR/playbooks ]; then + git clone $GIT_URL $WORKDIR/playbooks fi - pushd /home/cloud-admin/playbooks > /dev/null + pushd $WORKDIR/playbooks > /dev/null git fetch -af - # exclude any '-> HEAD'... lines if they exist in the supplied git repo - export LATEST_BRANCH=$(git branch --sort=-committerdate -lr | grep -v ' -> ' | head -n1 | sed -e 's|^ *||g') - if [ -z "$LATEST_BRANCH" ]; then - echo "The Git repo is empty, try again once the OpenStackConfigGenerator finishes and pushes a commit." - exit 1 - fi - if ! git tag | grep ^latest$ &>/dev/null; then - echo "Initializing 'latest' git tag with refs/remotes/$LATEST_BRANCH" - git tag latest refs/remotes/$LATEST_BRANCH - git push origin --tags - fi - popd > /dev/null -} - -diff() { - init - pushd /home/cloud-admin/playbooks > /dev/null - # NOTE: this excludes _server_id lines which get modified each time Heat runs - # NOTE: we ignore the inventory.yaml as it gets randomly sorted each time - # NOTE: we ignore ansible.cfg which changes each time - git difftool -y --extcmd="diff --recursive --unified=1 --color --ignore-matching-lines='_server_id:' $@" refs/tags/latest $LATEST_BRANCH -- . ':!*inventory.yaml' ':!*ansible.cfg' popd > /dev/null } play() { init set_env - pushd /home/cloud-admin/playbooks > /dev/null + pushd $WORKDIR/playbooks > /dev/null - if [ ! -d /home/cloud-admin/playbooks/tripleo-ansible ]; then + if [ ! -d $WORKDIR/playbooks/tripleo-ansible ]; then echo "Playbooks directory don't exist! Run the following command to accept and tag the new playbooks first:" echo " $0 -a" echo "" @@ -84,26 +80,15 @@ play() { exit fi - if ! git diff --exit-code --no-patch refs/tags/latest remotes/$LATEST_BRANCH; then - echo "New playbooks detected. Run the following command to view a diff of the changes:" - echo " $0 -d" - echo "" - echo "To accept and retag the new playbooks run:" - echo " $0 -a" - echo "" - echo "Then re-run '$0 -p' to run the new playbooks." - exit - fi - cd tripleo-ansible # TODO: for now disable opendev-validation # e.g. The check fails because the lvm2 package is not installed in openstackclient container image image # and ansible_facts include packages from undercloud. ansible-playbook \ - -i /home/cloud-admin/playbooks/tripleo-ansible/tripleo-ansible-inventory.yaml \ + -i $WORKDIR/playbooks/tripleo-ansible/tripleo-ansible-inventory.yaml \ --skip-tags opendev-validation \ - /home/cloud-admin/playbooks/tripleo-ansible/deploy_steps_playbook.yaml + $WORKDIR/playbooks/tripleo-ansible/deploy_steps_playbook.yaml mkdir -p ~/.config/openstack sudo cp -f /etc/openstack/clouds.yaml ~/.config/openstack/clouds.yaml @@ -114,10 +99,10 @@ play() { accept() { init - pushd /home/cloud-admin/playbooks > /dev/null - git tag -d latest - git push -f --delete origin refs/tags/latest - git tag latest refs/remotes/$LATEST_BRANCH + pushd $WORKDIR/playbooks > /dev/null + git tag -d latest || true + git push -f --delete origin refs/tags/latest || true + git tag latest remotes/origin/$CONFIG_VERSION git push origin --tags # checkout accepted code @@ -131,23 +116,5 @@ accept() { popd > /dev/null } -usage() { echo "Usage: $0 [-d] [-p] [-a]" 1>&2; exit 1; } - -[[ $# -eq 0 ]] && usage - -while getopts ":dpa" arg; do - case "${arg}" in - d) - diff - ;; - p) - play - ;; - a) - accept - ;; - *) - usage - ;; - esac -done +accept +play \ No newline at end of file