diff --git a/.gitignore b/.gitignore index 4780583..e34d81a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Binary /microshift-installer /microshift-installer.exe +/manager # Asset / state directories my-cluster/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8a8ee95 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# Build the manager binary +FROM golang:1.26 AS builder + +WORKDIR /workspace + +# Copy go.mod and go.sum first for better layer caching +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the rest of the source code +COPY . . + +# Build the manager binary +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager ./cmd/manager/ + +# Runtime image +FROM gcr.io/distroless/static:nonroot AS runtime +WORKDIR / + +COPY --from=builder /workspace/manager . + +USER 65532:65532 + +ENTRYPOINT ["/manager"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..362344a --- /dev/null +++ b/Makefile @@ -0,0 +1,74 @@ +# Build the manager binary +IMAGE_TAG ?= latest +IMG ?= microshift-installer:$(IMAGE_TAG) + +.PHONY: help +help: + @echo "Usage:" + @echo " make cli Build the CLI binary (microshift-installer)" + @echo " make build Build the operator manager binary" + @echo " make run Run the operator locally" + @echo " make docker-build Build the operator Docker image" + @echo " make deploy Deploy the operator to a K8s cluster" + @echo " make install Install CRDs into the cluster" + @echo " make uninstall Uninstall CRDs from the cluster" + @echo " make vet Run go vet" + @echo " make fmt Run go fmt" + +.PHONY: cli +cli: + go build -o bin/microshift-installer ./cmd/microshift-installer/ + +.PHONY: build +build: + go build -o bin/manager ./cmd/manager/ + +.PHONY: run +run: + go run ./cmd/manager/ + +.PHONY: docker-build +docker-build: + docker build -t $(IMG) . + +.PHONY: docker-push +docker-push: + docker push $(IMG) + +.PHONY: install +install: + kubectl apply -f config/crd/ + +.PHONY: uninstall +uninstall: + kubectl delete -f config/crd/ + +.PHONY: deploy +deploy: install + kubectl apply -f config/rbac/ + kubectl apply -f config/manager/ + +.PHONY: undeploy +undeploy: + kubectl delete -f config/manager/ + kubectl delete -f config/rbac/ + -kubectl delete -f config/crd/ + +.PHONY: vet +vet: + go vet ./... + +.PHONY: fmt +fmt: + gofmt -l . > fmt_output + @if [ -s fmt_output ]; then \ + echo "Unformatted files:"; \ + cat fmt_output; \ + rm fmt_output; \ + exit 1; \ + fi + @rm -f fmt_output + +.PHONY: clean +clean: + rm -rf bin/ diff --git a/README.md b/README.md index 0994a46..646dd6a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,15 @@ Unlike wrapping Terraform HCL, this installer uses the cloud provider's **native - **AWS**: [aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) — EC2, VPC, Route53 - **GCP**: [google-cloud-go](https://github.com/googleapis/google-cloud-go) / [google-api-go-client](https://github.com/googleapis/google-api-go-client) — Compute Engine, Cloud DNS -## Prerequisites +The project provides two modes: +- **CLI** (original) — run `microshift-installer` directly on your machine +- **Kubernetes Operator** — manage clusters declaratively via CRDs inside a Kubernetes cluster + +--- + +## CLI Mode + +### Prerequisites - **Go** 1.22+ (to build) - A **Red Hat subscription** (for RHEL + MicroShift) @@ -20,25 +28,23 @@ Cloud-specific: | **AWS** | AWS credentials (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` or `~/.aws/credentials`); Route53 public hosted zone | | **GCP** | GCP project with Compute Engine, Cloud DNS, and Cloud NAT APIs enabled; Cloud DNS managed zone; service account key (JSON) optional | -## Quick Start +### Quick Start ```bash -# Build -git clone https://github.com/hchenxa/microshift-installer -cd microshift-installer -go build -o microshift-installer ./cmd/microshift-installer +# Build the CLI +make cli # Create a cluster (interactive mode — prompts for all required fields) -./microshift-installer create cluster --dir=./my-cluster +./bin/microshift-installer create cluster --dir=./my-cluster # Or use a pre-written install-config.yaml (see examples/) -./microshift-installer create cluster --dir=./my-cluster +./bin/microshift-installer create cluster --dir=./my-cluster # Destroy -./microshift-installer destroy cluster --dir=./my-cluster +./bin/microshift-installer destroy cluster --dir=./my-cluster ``` -## Commands +### Commands | Command | Description | |---|---| @@ -48,9 +54,9 @@ go build -o microshift-installer ./cmd/microshift-installer If `--dir` is not provided, it defaults to `~/.microshift-installer//`. -## Configuration +### Configuration -### install-config.yaml +#### install-config.yaml Full reference — see [`examples/install-config.yaml.example`](examples/install-config.yaml.example) for a completed template. @@ -86,48 +92,11 @@ network: privateSubnetCIDR: 10.0.16.0/20 # Optional — default: 10.0.16.0/20 ``` -### Interactive mode +#### Interactive mode If no `install-config.yaml` exists in the asset directory, the installer starts an interactive questionnaire covering all required fields. -## Project Structure - -``` -microshift-installer/ -├── cmd/ -│ └── microshift-installer/ -│ └── main.go ← CLI entry point (cobra) -├── pkg/ -│ ├── types/ -│ │ └── installconfig.go ← InstallConfig struct + validation -│ ├── config/ -│ │ ├── installconfig.go ← YAML parse/save -│ │ └── interactive.go ← Interactive prompts (survey) -│ ├── asset/ -│ │ └── store.go ← Asset directory management -│ ├── state/ -│ │ └── state.go ← Cluster resource state (JSON) -│ └── platform/ -│ ├── platform.go ← Platform interface -│ ├── aws.go ← AWS: EC2, VPC, Route53 via aws-sdk-go-v2 -│ └── gcp.go ← GCP: Compute Engine, DNS via google-cloud-go -├── examples/ -│ └── install-config.yaml.example ← Example config template -├── go.mod / go.sum -└── README.md -``` - -## How It Works - -1. **Config**: Loads `install-config.yaml` from the asset directory (or interactively creates one) -2. **Create**: The selected platform implementation calls cloud SDK APIs directly: - - AWS: CreateVPC → CreateInternetGateway → CreateSubnet → CreateRouteTable → CreateSecurityGroup → ImportKeyPair → RunInstances → Route53 Upsert - - GCP: InsertNetwork → InsertSubnetwork → InsertRouter/NAT → InsertFirewall → InsertInstance → DNS Create -3. **State**: Every created resource is recorded in `cluster-state.json` in the asset directory -4. **Output**: Displays the node public IP, SSH command, and kubeconfig retrieval command -5. **Destroy**: Reads the state file and deletes resources in reverse creation order - -## State Management +### State Management The `cluster-state.json` file tracks every cloud resource. It is critical for destruction — keep it safe alongside the asset directory. @@ -140,20 +109,192 @@ The `cluster-state.json` file tracks every cloud resource. It is critical for de } ``` -## Building from Source +--- -```bash -go build -o microshift-installer ./cmd/microshift-installer +## Kubernetes Operator Mode + +Manage MicroShift clusters declaratively via Custom Resources inside a Kubernetes cluster. + +### Architecture + +``` +┌──────────────────────────────────────────────┐ +│ K8s API Server │ +│ │ +│ MicroShiftDeployment (CR) │ +│ ┌──────────────────────────────┐ │ +│ │ spec: │ │ +│ │ installConfigRef → ConfigMap│ │ +│ │ autoDestroy: true │ │ +│ │ status: │ │ +│ │ phase: Ready/Failed │ │ +│ └──────────┬───────────────────┘ │ +│ │ creates/owns │ +│ ▼ │ +│ MicroShiftProvisioner (CR) │ +│ ┌──────────────────────────────┐ │ +│ │ spec: │ │ +│ │ operation: Create/Destroy │ │ +│ │ installConfig: {...} │ │ +│ │ status: │ │ +│ │ phase: Running/Succeeded │ │ +│ │ resources: [...] │ │ +│ └──────────┬───────────────────┘ │ +│ │ runs async │ +│ ▼ │ +│ ┌──────────────────────┐ │ +│ │ Provisioner Controller│──→ AWS/GCP SDK │ +│ └──────────────────────┘ │ +│ │ +│ ConfigMap (install-config.yaml) │ +│ Secret (GCP SA Key) │ +└──────────────────────────────────────────────┘ +``` + +### CRDs + +#### MicroShiftDeployment (`ms-deployment`) + +The user-facing CR that describes a desired cluster deployment. + +```yaml +apiVersion: cluster-install.io/v1alpha1 +kind: MicroShiftDeployment +metadata: + name: my-cluster +spec: + clusterName: my-cluster + installConfigRef: + name: my-cluster-install-config + autoDestroy: true +``` + +#### MicroShiftProvisioner (`ms-provisioner`) + +An internal CR that manages the actual provisioning operation. Created automatically by the deployment controller. + +```yaml +apiVersion: cluster-install.io/v1alpha1 +kind: MicroShiftProvisioner +spec: + clusterName: my-cluster + operation: Create # or "Destroy" + installConfig: "..." # deserialised from ConfigMap + cloudProvider: aws ``` -To cross-compile: +### Workflow + +1. **Create**: User creates `MicroShiftDeployment` + ConfigMap → controller creates `MicroShiftProvisioner(operation=Create)` → async provisioning → status updated to `Ready` +2. **Destroy** (auto): User deletes `MicroShiftDeployment` → finalizer blocks deletion → controller creates `MicroShiftProvisioner(operation=Destroy)` → resources torn down → finalizer removed + +### Deploy the Operator ```bash -GOOS=linux GOARCH=amd64 go build -o microshift-installer ./cmd/microshift-installer +# 1. Build the operator image +make docker-build IMG=my-registry/microshift-installer:latest + +# 2. Push to your registry +make docker-push IMG=my-registry/microshift-installer:latest + +# 3. Update config/manager/manager.yaml with your image name + +# 4. Deploy to cluster +make deploy + +# 5. Create an install-config ConfigMap +kubectl apply -f config/samples/install-config.yaml + +# 6. Create a MicroShiftDeployment +kubectl apply -f config/samples/microshiftdeployment_sample.yaml + +# 7. Watch progress +kubectl get msd -w +kubectl get msp -w ``` -## Running Tests +### Cloud Credentials + +Configure cloud credentials in the operator's Deployment (`config/manager/manager.yaml`): + +```yaml +env: + # AWS + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: aws-credentials + key: access-key-id + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: aws-credentials + key: secret-access-key + # GCP + - name: GOOGLE_APPLICATION_CREDENTIALS + value: /etc/gcp/sa-key.json +volumeMounts: + - name: gcp-sa-key + mountPath: /etc/gcp + readOnly: true +``` + +--- + +## Building ```bash -go test ./... +# CLI binary (to bin/microshift-installer) +make cli + +# Operator manager binary (to bin/manager) +make build + +# Operator Docker image +make docker-build +``` + +## Project Structure + +``` +microshift-installer/ +├── cmd/ +│ ├── microshift-installer/ +│ │ └── main.go ← CLI entry point (cobra) +│ └── manager/ +│ └── main.go ← Operator entry point (controller-runtime) +├── api/ +│ └── v1alpha1/ ← CRD type definitions +│ ├── groupversion_info.go +│ ├── microshiftdeployment_types.go +│ ├── microshiftprovisioner_types.go +│ └── zz_generated.deepcopy.go +├── controllers/ +│ ├── microshiftdeployment_controller.go +│ └── microshiftprovisioner_controller.go +├── config/ +│ ├── crd/ ← CRD YAML manifests +│ ├── rbac/ ← RBAC resources +│ ├── manager/ ← Operator Deployment +│ └── samples/ ← Example CRs +├── pkg/ +│ ├── types/ +│ │ └── installconfig.go ← InstallConfig struct + validation +│ ├── config/ +│ │ ├── installconfig.go ← YAML parse/save +│ │ └── interactive.go ← Interactive prompts (survey) +│ ├── asset/ +│ │ └── store.go ← Asset directory management +│ ├── state/ +│ │ └── state.go ← Cluster resource state (JSON) +│ └── platform/ +│ ├── platform.go ← Platform interface +│ ├── aws.go ← AWS: EC2, VPC, Route53 via aws-sdk-go-v2 +│ └── gcp.go ← GCP: Compute Engine, DNS via google-cloud-go +├── examples/ +│ └── install-config.yaml.example +├── Dockerfile +├── Makefile +├── go.mod / go.sum +└── README.md ``` diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..2c83769 --- /dev/null +++ b/api/v1alpha1/groupversion_info.go @@ -0,0 +1,17 @@ +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is the group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "cluster-install.io", Version: "v1alpha1"} + + // SchemeBuilder is used to add Go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v1alpha1/microshiftdeployment_types.go b/api/v1alpha1/microshiftdeployment_types.go new file mode 100644 index 0000000..0305c06 --- /dev/null +++ b/api/v1alpha1/microshiftdeployment_types.go @@ -0,0 +1,160 @@ +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// MicroShiftDeploymentPhase describes the lifecycle phase of a deployment. +type MicroShiftDeploymentPhase string + +const ( + // DeploymentPhasePending means the deployment has been created but provisioning + // has not yet started. + DeploymentPhasePending MicroShiftDeploymentPhase = "Pending" + // DeploymentPhaseProvisioning means a provisioner is actively creating resources. + DeploymentPhaseProvisioning MicroShiftDeploymentPhase = "Provisioning" + // DeploymentPhaseReady means the cluster is fully provisioned and available. + DeploymentPhaseReady MicroShiftDeploymentPhase = "Ready" + // DeploymentPhaseDeleting means the cluster is being torn down. + DeploymentPhaseDeleting MicroShiftDeploymentPhase = "Deleting" + // DeploymentPhaseFailed means provisioning or destruction has failed. + DeploymentPhaseFailed MicroShiftDeploymentPhase = "Failed" +) + +const ( + // MicroShiftDeploymentFinalizer is used to ensure cluster resources are destroyed + // before the CR is deleted. + MicroShiftDeploymentFinalizer = "microshiftdeployment.cluster-install.io/finalizer" +) + +// ConfigMapRef references a ConfigMap by name in the same namespace. +type ConfigMapRef struct { + // Name is the name of the ConfigMap containing install-config.yaml. + Name string `json:"name"` +} + +// SecretRef references a Secret by name in the same namespace. +type SecretRef struct { + // Name is the name of the Secret. + Name string `json:"name"` +} + +// CredentialsRef holds references to cloud-provider credential secrets. +type CredentialsRef struct { + // GCPServiceAccountKey references a Secret containing a GCP service account key. + // The Secret should have a key named "service-account.json". + // +optional + GCPServiceAccountKey *SecretRef `json:"gcpServiceAccountKey,omitempty"` +} + +// ClusterInfo holds post-creation information about the provisioned cluster. +type ClusterInfo struct { + // NodePublicIP is the public IP address of the MicroShift node. + // +optional + NodePublicIP string `json:"nodePublicIP,omitempty"` + // SSHCommand is a shell command to SSH into the node. + // +optional + SSHCommand string `json:"sshCommand,omitempty"` + // KubeconfigCommand is a shell command to retrieve the kubeconfig. + // +optional + KubeconfigCommand string `json:"kubeconfigCommand,omitempty"` + // DNSFQDN is the fully qualified DNS name of the cluster, if configured. + // +optional + DNSFQDN string `json:"dnsFQDN,omitempty"` +} + +// ResourceRecord represents a single cloud resource created for this cluster. +type ResourceRecord struct { + // Name is the logical name (e.g. "vpc", "microshift_node"). + Name string `json:"name"` + // Type is the resource type (e.g. "aws_vpc", "gcp_compute_instance"). + Type string `json:"type"` + // Provider is the cloud provider ("aws" or "gcp"). + Provider string `json:"provider"` + // ID is the cloud resource identifier. + ID string `json:"id"` +} + +// MicroShiftDeploymentSpec defines the desired state of a MicroShift deployment. +type MicroShiftDeploymentSpec struct { + // ClusterName is used for naming cloud resources. Defaults to metadata.name. + // +optional + ClusterName string `json:"clusterName,omitempty"` + + // InstallConfigRef references a ConfigMap containing install-config.yaml. + // The ConfigMap must exist in the same namespace as this CR. + // +optional + InstallConfigRef *ConfigMapRef `json:"installConfigRef,omitempty"` + + // CredentialsRef holds references to cloud-provider credential secrets. + // +optional + CredentialsRef *CredentialsRef `json:"credentialsRef,omitempty"` + + // ProvisionTimeoutSeconds is the timeout for provisioning operations. + // Defaults to 1800 (30 minutes). + // +optional + ProvisionTimeoutSeconds *int32 `json:"provisionTimeoutSeconds,omitempty"` + + // AutoDestroy controls whether cloud resources are automatically destroyed + // when this CR is deleted. Defaults to true. + // +optional + AutoDestroy *bool `json:"autoDestroy,omitempty"` +} + +// MicroShiftDeploymentStatus defines the observed state. +type MicroShiftDeploymentStatus struct { + // Phase is the current lifecycle phase. + // +optional + Phase MicroShiftDeploymentPhase `json:"phase,omitempty"` + + // Conditions represent the latest available observations. + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // ProvisionerRef references the active MicroShiftProvisioner managing this deployment. + // +optional + ProvisionerRef *corev1.ObjectReference `json:"provisionerRef,omitempty"` + + // ClusterInfo holds post-creation cluster details. + // +optional + ClusterInfo *ClusterInfo `json:"clusterInfo,omitempty"` + + // Resources lists all cloud resources created for this cluster. + // This is used during destroy to know which resources to tear down. + // +optional + Resources []ResourceRecord `json:"resources,omitempty"` + + // ObservedGeneration is the last generation that was reconciled. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:object:generate=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="Current phase" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// MicroShiftDeployment is the Schema for managing MicroShift cluster deployments. +type MicroShiftDeployment struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MicroShiftDeploymentSpec `json:"spec,omitempty"` + Status MicroShiftDeploymentStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:object:generate=true + +// MicroShiftDeploymentList contains a list of MicroShiftDeployment. +type MicroShiftDeploymentList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MicroShiftDeployment `json:"items"` +} + +func init() { + SchemeBuilder.Register(&MicroShiftDeployment{}, &MicroShiftDeploymentList{}) +} diff --git a/api/v1alpha1/microshiftdeprovisioner_types.go b/api/v1alpha1/microshiftdeprovisioner_types.go new file mode 100644 index 0000000..8f5224a --- /dev/null +++ b/api/v1alpha1/microshiftdeprovisioner_types.go @@ -0,0 +1,83 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// DeprovisionerPhase describes the lifecycle phase of a deprovisioner. +type DeprovisionerPhase string + +const ( + // DeprovisionerPhasePending means work has not yet started. + DeprovisionerPhasePending DeprovisionerPhase = "Pending" + // DeprovisionerPhaseRunning means resources are being destroyed. + DeprovisionerPhaseRunning DeprovisionerPhase = "Running" + // DeprovisionerPhaseSucceeded means destruction completed successfully. + DeprovisionerPhaseSucceeded DeprovisionerPhase = "Succeeded" + // DeprovisionerPhaseFailed means destruction failed. + DeprovisionerPhaseFailed DeprovisionerPhase = "Failed" +) + +// MicroShiftDeprovisionerSpec defines the desired state of a deprovisioner. +type MicroShiftDeprovisionerSpec struct { + // ClusterName is the name of the cluster to destroy. + ClusterName string `json:"clusterName"` + + // CloudProvider is "aws" or "gcp". + CloudProvider string `json:"cloudProvider"` + + // InstallConfig is the install-config YAML (used for region/project settings). + // +optional + InstallConfig string `json:"installConfig,omitempty"` + + // Resources lists the cloud resources to destroy. + // +optional + Resources []ResourceRecord `json:"resources,omitempty"` + + // CredentialsRef holds references to cloud-provider credential secrets. + // +optional + CredentialsRef *CredentialsRef `json:"credentialsRef,omitempty"` +} + +// MicroShiftDeprovisionerStatus defines the observed state. +type MicroShiftDeprovisionerStatus struct { + // Phase is the current operation phase. + // +optional + Phase DeprovisionerPhase `json:"phase,omitempty"` + + // Conditions represent the latest available observations. + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // ObservedGeneration is the last generation that was reconciled. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:object:generate=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="Current phase" + +// MicroShiftDeprovisioner is the Schema for destroying cluster resources. +type MicroShiftDeprovisioner struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MicroShiftDeprovisionerSpec `json:"spec,omitempty"` + Status MicroShiftDeprovisionerStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:object:generate=true + +// MicroShiftDeprovisionerList contains a list of MicroShiftDeprovisioner. +type MicroShiftDeprovisionerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MicroShiftDeprovisioner `json:"items"` +} + +func init() { + SchemeBuilder.Register(&MicroShiftDeprovisioner{}, &MicroShiftDeprovisionerList{}) +} diff --git a/api/v1alpha1/microshiftprovisioner_types.go b/api/v1alpha1/microshiftprovisioner_types.go new file mode 100644 index 0000000..5d654b0 --- /dev/null +++ b/api/v1alpha1/microshiftprovisioner_types.go @@ -0,0 +1,89 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ProvisionerPhase describes the lifecycle phase of a provisioner. +type ProvisionerPhase string + +const ( + // ProvisionerPhasePending means the provisioner has been created but work + // has not yet started. + ProvisionerPhasePending ProvisionerPhase = "Pending" + // ProvisionerPhaseRunning means the provisioner is actively creating resources. + ProvisionerPhaseRunning ProvisionerPhase = "Running" + // ProvisionerPhaseSucceeded means the operation completed successfully. + ProvisionerPhaseSucceeded ProvisionerPhase = "Succeeded" + // ProvisionerPhaseFailed means the operation failed. + ProvisionerPhaseFailed ProvisionerPhase = "Failed" +) + +// MicroShiftProvisionerSpec defines the desired state of a provisioner. +type MicroShiftProvisionerSpec struct { + // ClusterName is the name of the cluster (used for resource naming). + ClusterName string `json:"clusterName"` + + // InstallConfig is the full install configuration, serialised from the + // referenced ConfigMap by the deployment controller. + // +optional + InstallConfig string `json:"installConfig,omitempty"` + + // CloudProvider is "aws" or "gcp". + CloudProvider string `json:"cloudProvider"` + + // CredentialsRef holds references to cloud-provider credential secrets. + // +optional + CredentialsRef *CredentialsRef `json:"credentialsRef,omitempty"` +} + +// MicroShiftProvisionerStatus defines the observed state. +type MicroShiftProvisionerStatus struct { + // Phase is the current operation phase. + // +optional + Phase ProvisionerPhase `json:"phase,omitempty"` + + // Conditions represent the latest available observations. + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // ClusterInfo holds post-creation cluster details. + // +optional + ClusterInfo *ClusterInfo `json:"clusterInfo,omitempty"` + + // Resources lists the cloud resources created for this cluster. + // +optional + Resources []ResourceRecord `json:"resources,omitempty"` + + // ObservedGeneration is the last generation that was reconciled. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:object:generate=true +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="Current phase" + +// MicroShiftProvisioner is the Schema for creating cluster resources. +type MicroShiftProvisioner struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MicroShiftProvisionerSpec `json:"spec,omitempty"` + Status MicroShiftProvisionerStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:object:generate=true + +// MicroShiftProvisionerList contains a list of MicroShiftProvisioner. +type MicroShiftProvisionerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MicroShiftProvisioner `json:"items"` +} + +func init() { + SchemeBuilder.Register(&MicroShiftProvisioner{}, &MicroShiftProvisionerList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..191f15c --- /dev/null +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,466 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// ────────────────────────────────────────────────────────────────────────────── +// ConfigMapRef +// ────────────────────────────────────────────────────────────────────────────── + +func (in *ConfigMapRef) DeepCopyInto(out *ConfigMapRef) { + *out = *in +} + +func (in *ConfigMapRef) DeepCopy() *ConfigMapRef { + if in == nil { + return nil + } + out := new(ConfigMapRef) + in.DeepCopyInto(out) + return out +} + +// ────────────────────────────────────────────────────────────────────────────── +// SecretRef +// ────────────────────────────────────────────────────────────────────────────── + +func (in *SecretRef) DeepCopyInto(out *SecretRef) { + *out = *in +} + +func (in *SecretRef) DeepCopy() *SecretRef { + if in == nil { + return nil + } + out := new(SecretRef) + in.DeepCopyInto(out) + return out +} + +// ────────────────────────────────────────────────────────────────────────────── +// CredentialsRef +// ────────────────────────────────────────────────────────────────────────────── + +func (in *CredentialsRef) DeepCopyInto(out *CredentialsRef) { + *out = *in + if in.GCPServiceAccountKey != nil { + in, out := &in.GCPServiceAccountKey, &out.GCPServiceAccountKey + *out = new(SecretRef) + **out = **in + } +} + +func (in *CredentialsRef) DeepCopy() *CredentialsRef { + if in == nil { + return nil + } + out := new(CredentialsRef) + in.DeepCopyInto(out) + return out +} + +// ────────────────────────────────────────────────────────────────────────────── +// ClusterInfo +// ────────────────────────────────────────────────────────────────────────────── + +func (in *ClusterInfo) DeepCopyInto(out *ClusterInfo) { + *out = *in +} + +func (in *ClusterInfo) DeepCopy() *ClusterInfo { + if in == nil { + return nil + } + out := new(ClusterInfo) + in.DeepCopyInto(out) + return out +} + +// ────────────────────────────────────────────────────────────────────────────── +// ResourceRecord +// ────────────────────────────────────────────────────────────────────────────── + +func (in *ResourceRecord) DeepCopyInto(out *ResourceRecord) { + *out = *in +} + +func (in *ResourceRecord) DeepCopy() *ResourceRecord { + if in == nil { + return nil + } + out := new(ResourceRecord) + in.DeepCopyInto(out) + return out +} + +// ────────────────────────────────────────────────────────────────────────────── +// MicroShiftDeploymentSpec +// ────────────────────────────────────────────────────────────────────────────── + +func (in *MicroShiftDeploymentSpec) DeepCopyInto(out *MicroShiftDeploymentSpec) { + *out = *in + if in.InstallConfigRef != nil { + in, out := &in.InstallConfigRef, &out.InstallConfigRef + *out = new(ConfigMapRef) + **out = **in + } + if in.CredentialsRef != nil { + in, out := &in.CredentialsRef, &out.CredentialsRef + *out = new(CredentialsRef) + (*in).DeepCopyInto(*out) + } + if in.ProvisionTimeoutSeconds != nil { + in, out := &in.ProvisionTimeoutSeconds, &out.ProvisionTimeoutSeconds + *out = new(int32) + **out = **in + } + if in.AutoDestroy != nil { + in, out := &in.AutoDestroy, &out.AutoDestroy + *out = new(bool) + **out = **in + } +} + +func (in *MicroShiftDeploymentSpec) DeepCopy() *MicroShiftDeploymentSpec { + if in == nil { + return nil + } + out := new(MicroShiftDeploymentSpec) + in.DeepCopyInto(out) + return out +} + +// ────────────────────────────────────────────────────────────────────────────── +// MicroShiftDeploymentStatus +// ────────────────────────────────────────────────────────────────────────────── + +func (in *MicroShiftDeploymentStatus) DeepCopyInto(out *MicroShiftDeploymentStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ProvisionerRef != nil { + in, out := &in.ProvisionerRef, &out.ProvisionerRef + *out = new(corev1.ObjectReference) + **out = **in + } + if in.ClusterInfo != nil { + in, out := &in.ClusterInfo, &out.ClusterInfo + *out = new(ClusterInfo) + **out = **in + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]ResourceRecord, len(*in)) + copy(*out, *in) + } +} + +func (in *MicroShiftDeploymentStatus) DeepCopy() *MicroShiftDeploymentStatus { + if in == nil { + return nil + } + out := new(MicroShiftDeploymentStatus) + in.DeepCopyInto(out) + return out +} + +// ────────────────────────────────────────────────────────────────────────────── +// MicroShiftDeployment +// ────────────────────────────────────────────────────────────────────────────── + +func (in *MicroShiftDeployment) DeepCopyInto(out *MicroShiftDeployment) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +func (in *MicroShiftDeployment) DeepCopy() *MicroShiftDeployment { + if in == nil { + return nil + } + out := new(MicroShiftDeployment) + in.DeepCopyInto(out) + return out +} + +func (in *MicroShiftDeployment) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// ────────────────────────────────────────────────────────────────────────────── +// MicroShiftDeploymentList +// ────────────────────────────────────────────────────────────────────────────── + +func (in *MicroShiftDeploymentList) DeepCopyInto(out *MicroShiftDeploymentList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]MicroShiftDeployment, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +func (in *MicroShiftDeploymentList) DeepCopy() *MicroShiftDeploymentList { + if in == nil { + return nil + } + out := new(MicroShiftDeploymentList) + in.DeepCopyInto(out) + return out +} + +func (in *MicroShiftDeploymentList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// ────────────────────────────────────────────────────────────────────────────── +// MicroShiftProvisionerSpec +// ────────────────────────────────────────────────────────────────────────────── + +func (in *MicroShiftProvisionerSpec) DeepCopyInto(out *MicroShiftProvisionerSpec) { + *out = *in + if in.CredentialsRef != nil { + in, out := &in.CredentialsRef, &out.CredentialsRef + *out = new(CredentialsRef) + (*in).DeepCopyInto(*out) + } +} + +func (in *MicroShiftProvisionerSpec) DeepCopy() *MicroShiftProvisionerSpec { + if in == nil { + return nil + } + out := new(MicroShiftProvisionerSpec) + in.DeepCopyInto(out) + return out +} + +// ────────────────────────────────────────────────────────────────────────────── +// MicroShiftProvisionerStatus +// ────────────────────────────────────────────────────────────────────────────── + +func (in *MicroShiftProvisionerStatus) DeepCopyInto(out *MicroShiftProvisionerStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ClusterInfo != nil { + in, out := &in.ClusterInfo, &out.ClusterInfo + *out = new(ClusterInfo) + **out = **in + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]ResourceRecord, len(*in)) + copy(*out, *in) + } +} + +func (in *MicroShiftProvisionerStatus) DeepCopy() *MicroShiftProvisionerStatus { + if in == nil { + return nil + } + out := new(MicroShiftProvisionerStatus) + in.DeepCopyInto(out) + return out +} + +// ────────────────────────────────────────────────────────────────────────────── +// MicroShiftProvisioner +// ────────────────────────────────────────────────────────────────────────────── + +func (in *MicroShiftProvisioner) DeepCopyInto(out *MicroShiftProvisioner) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +func (in *MicroShiftProvisioner) DeepCopy() *MicroShiftProvisioner { + if in == nil { + return nil + } + out := new(MicroShiftProvisioner) + in.DeepCopyInto(out) + return out +} + +func (in *MicroShiftProvisioner) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// ────────────────────────────────────────────────────────────────────────────── +// MicroShiftProvisionerList +// ────────────────────────────────────────────────────────────────────────────── + +func (in *MicroShiftProvisionerList) DeepCopyInto(out *MicroShiftProvisionerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]MicroShiftProvisioner, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +func (in *MicroShiftProvisionerList) DeepCopy() *MicroShiftProvisionerList { + if in == nil { + return nil + } + out := new(MicroShiftProvisionerList) + in.DeepCopyInto(out) + return out +} + +func (in *MicroShiftProvisionerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// ────────────────────────────────────────────────────────────────────────────── +// MicroShiftDeprovisionerSpec +// ────────────────────────────────────────────────────────────────────────────── + +func (in *MicroShiftDeprovisionerSpec) DeepCopyInto(out *MicroShiftDeprovisionerSpec) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]ResourceRecord, len(*in)) + copy(*out, *in) + } + if in.CredentialsRef != nil { + in, out := &in.CredentialsRef, &out.CredentialsRef + *out = new(CredentialsRef) + (*in).DeepCopyInto(*out) + } +} + +func (in *MicroShiftDeprovisionerSpec) DeepCopy() *MicroShiftDeprovisionerSpec { + if in == nil { + return nil + } + out := new(MicroShiftDeprovisionerSpec) + in.DeepCopyInto(out) + return out +} + +// ────────────────────────────────────────────────────────────────────────────── +// MicroShiftDeprovisionerStatus +// ────────────────────────────────────────────────────────────────────────────── + +func (in *MicroShiftDeprovisionerStatus) DeepCopyInto(out *MicroShiftDeprovisionerStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]metav1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +func (in *MicroShiftDeprovisionerStatus) DeepCopy() *MicroShiftDeprovisionerStatus { + if in == nil { + return nil + } + out := new(MicroShiftDeprovisionerStatus) + in.DeepCopyInto(out) + return out +} + +// ────────────────────────────────────────────────────────────────────────────── +// MicroShiftDeprovisioner +// ────────────────────────────────────────────────────────────────────────────── + +func (in *MicroShiftDeprovisioner) DeepCopyInto(out *MicroShiftDeprovisioner) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +func (in *MicroShiftDeprovisioner) DeepCopy() *MicroShiftDeprovisioner { + if in == nil { + return nil + } + out := new(MicroShiftDeprovisioner) + in.DeepCopyInto(out) + return out +} + +func (in *MicroShiftDeprovisioner) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// ────────────────────────────────────────────────────────────────────────────── +// MicroShiftDeprovisionerList +// ────────────────────────────────────────────────────────────────────────────── + +func (in *MicroShiftDeprovisionerList) DeepCopyInto(out *MicroShiftDeprovisionerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]MicroShiftDeprovisioner, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +func (in *MicroShiftDeprovisionerList) DeepCopy() *MicroShiftDeprovisionerList { + if in == nil { + return nil + } + out := new(MicroShiftDeprovisionerList) + in.DeepCopyInto(out) + return out +} + +func (in *MicroShiftDeprovisionerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/cmd/manager/main.go b/cmd/manager/main.go new file mode 100644 index 0000000..4050df0 --- /dev/null +++ b/cmd/manager/main.go @@ -0,0 +1,101 @@ +package main + +import ( + "flag" + "os" + + // Import all Kubernetes client auth plugins (e.g. for GCP, AWS, Azure) + _ "k8s.io/client-go/plugin/pkg/client/auth" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + v1alpha1 "github.com/hchenxa/microshift-installer/api/v1alpha1" + "github.com/hchenxa/microshift-installer/controllers" +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(v1alpha1.AddToScheme(scheme)) +} + +func main() { + var ( + metricsAddr string + probeAddr string + enableLeaderElection bool + ) + + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "Address for metrics endpoint") + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "Address for health probe endpoint") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, + "Enable leader election for controller manager (enables HA)") + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseDevMode(true))) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: server.Options{ + BindAddress: metricsAddr, + }, + HealthProbeBindAddress: probeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "microshift-installer.cluster-install.io", + }) + if err != nil { + setupLog.Error(err, "Unable to start manager") + os.Exit(1) + } + + // ── Register controllers ─────────────────────────────────────────── + if err := (&controllers.MicroShiftDeploymentReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "Unable to create controller", "controller", "MicroShiftDeployment") + os.Exit(1) + } + + if err := (&controllers.MicroShiftProvisionerReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "Unable to create controller", "controller", "MicroShiftProvisioner") + os.Exit(1) + } + + if err := (&controllers.MicroShiftDeprovisionerReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "Unable to create controller", "controller", "MicroShiftDeprovisioner") + os.Exit(1) + } + + // ── Health probes ────────────────────────────────────────────────── + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "Unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "Unable to set up ready check") + os.Exit(1) + } + + setupLog.Info("Starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "Problem running manager") + os.Exit(1) + } +} diff --git a/config/crd/cluster-install.io_microshiftdeployments.yaml b/config/crd/cluster-install.io_microshiftdeployments.yaml new file mode 100644 index 0000000..3ac9df7 --- /dev/null +++ b/config/crd/cluster-install.io_microshiftdeployments.yaml @@ -0,0 +1,172 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: microshiftdeployments.cluster-install.io +spec: + group: cluster-install.io + names: + kind: MicroShiftDeployment + listKind: MicroShiftDeploymentList + plural: microshiftdeployments + singular: microshiftdeployment + shortNames: + - msd + - ms-deployment + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Phase + type: string + jsonPath: .status.phase + description: Current phase + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + schema: + openAPIV3Schema: + type: object + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + properties: + name: + type: string + namespace: + type: string + spec: + type: object + properties: + clusterName: + type: string + description: Cluster name used for naming cloud resources. Defaults to metadata.name. + installConfigRef: + type: object + description: Reference to a ConfigMap containing install-config.yaml. + properties: + name: + type: string + description: Name of the ConfigMap. + required: + - name + credentialsRef: + type: object + description: References to cloud-provider credential secrets. + properties: + gcpServiceAccountKey: + type: object + description: Secret containing GCP service account key. + properties: + name: + type: string + description: Name of the Secret. + required: + - name + provisionTimeoutSeconds: + type: integer + format: int32 + description: Timeout for provisioning operations. Defaults to 1800. + autoDestroy: + type: boolean + description: Auto-destroy cloud resources when CR is deleted. Defaults to true. + status: + type: object + properties: + phase: + type: string + description: Current lifecycle phase. + enum: + - Pending + - Provisioning + - Ready + - Deleting + - Failed + conditions: + type: array + description: Latest available observations. + items: + type: object + required: + - type + - status + - lastTransitionTime + - reason + - message + properties: + type: + type: string + status: + type: string + enum: ["True", "False", "Unknown"] + observedGeneration: + type: integer + format: int64 + lastTransitionTime: + type: string + format: date-time + reason: + type: string + message: + type: string + provisionerRef: + type: object + description: Reference to the active MicroShiftProvisioner. + x-kubernetes-object-type: objectReference + properties: + kind: + type: string + namespace: + type: string + name: + type: string + uid: + type: string + apiVersion: + type: string + resourceVersion: + type: string + clusterInfo: + type: object + description: Post-creation cluster details. + properties: + nodePublicIP: + type: string + sshCommand: + type: string + kubeconfigCommand: + type: string + dnsFQDN: + type: string + resources: + type: array + description: Cloud resources created for this cluster. + items: + type: object + required: + - name + - type + - provider + - id + properties: + name: + type: string + type: + type: string + provider: + type: string + id: + type: string + observedGeneration: + type: integer + format: int64 + description: Last generation that was reconciled. + conversion: + strategy: None diff --git a/config/crd/cluster-install.io_microshiftdeprovisioners.yaml b/config/crd/cluster-install.io_microshiftdeprovisioners.yaml new file mode 100644 index 0000000..47bca79 --- /dev/null +++ b/config/crd/cluster-install.io_microshiftdeprovisioners.yaml @@ -0,0 +1,131 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: microshiftdeprovisioners.cluster-install.io +spec: + group: cluster-install.io + names: + kind: MicroShiftDeprovisioner + listKind: MicroShiftDeprovisionerList + plural: microshiftdeprovisioners + singular: microshiftdeprovisioner + shortNames: + - msdeprov + - ms-deprovisioner + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Phase + type: string + jsonPath: .status.phase + description: Current phase + schema: + openAPIV3Schema: + type: object + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + properties: + name: + type: string + namespace: + type: string + spec: + type: object + required: + - clusterName + - cloudProvider + properties: + clusterName: + type: string + description: Cluster name to destroy. + cloudProvider: + type: string + description: Cloud provider ("aws" or "gcp"). + enum: + - aws + - gcp + installConfig: + type: string + description: Install config YAML (for region/project settings). + resources: + type: array + description: Cloud resources to destroy. + items: + type: object + required: + - name + - type + - provider + - id + properties: + name: + type: string + type: + type: string + provider: + type: string + id: + type: string + credentialsRef: + type: object + description: References to cloud-provider credential secrets. + properties: + gcpServiceAccountKey: + type: object + properties: + name: + type: string + required: + - name + status: + type: object + properties: + phase: + type: string + description: Current operation phase. + enum: + - Pending + - Running + - Succeeded + - Failed + conditions: + type: array + items: + type: object + required: + - type + - status + - lastTransitionTime + - reason + - message + properties: + type: + type: string + status: + type: string + enum: ["True", "False", "Unknown"] + observedGeneration: + type: integer + format: int64 + lastTransitionTime: + type: string + format: date-time + reason: + type: string + message: + type: string + observedGeneration: + type: integer + format: int64 + conversion: + strategy: None diff --git a/config/crd/cluster-install.io_microshiftprovisioners.yaml b/config/crd/cluster-install.io_microshiftprovisioners.yaml new file mode 100644 index 0000000..df63fe5 --- /dev/null +++ b/config/crd/cluster-install.io_microshiftprovisioners.yaml @@ -0,0 +1,146 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: microshiftprovisioners.cluster-install.io +spec: + group: cluster-install.io + names: + kind: MicroShiftProvisioner + listKind: MicroShiftProvisionerList + plural: microshiftprovisioners + singular: microshiftprovisioner + shortNames: + - msp + - ms-provisioner + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + subresources: + status: {} + additionalPrinterColumns: + - name: Phase + type: string + jsonPath: .status.phase + description: Current phase + schema: + openAPIV3Schema: + type: object + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + properties: + name: + type: string + namespace: + type: string + spec: + type: object + required: + - clusterName + - cloudProvider + properties: + clusterName: + type: string + description: Cluster name used for resource naming. + installConfig: + type: string + description: Full install configuration YAML. + cloudProvider: + type: string + description: Cloud provider ("aws" or "gcp"). + enum: + - aws + - gcp + credentialsRef: + type: object + description: References to cloud-provider credential secrets. + properties: + gcpServiceAccountKey: + type: object + description: Secret containing GCP service account key. + properties: + name: + type: string + required: + - name + status: + type: object + properties: + phase: + type: string + description: Current operation phase. + enum: + - Pending + - Running + - Succeeded + - Failed + conditions: + type: array + description: Latest available observations. + items: + type: object + required: + - type + - status + - lastTransitionTime + - reason + - message + properties: + type: + type: string + status: + type: string + enum: ["True", "False", "Unknown"] + observedGeneration: + type: integer + format: int64 + lastTransitionTime: + type: string + format: date-time + reason: + type: string + message: + type: string + clusterInfo: + type: object + description: Post-creation cluster details. + properties: + nodePublicIP: + type: string + sshCommand: + type: string + kubeconfigCommand: + type: string + dnsFQDN: + type: string + resources: + type: array + description: Cloud resources created for this cluster. + items: + type: object + required: + - name + - type + - provider + - id + properties: + name: + type: string + type: + type: string + provider: + type: string + id: + type: string + observedGeneration: + type: integer + format: int64 + description: Last generation that was reconciled. + conversion: + strategy: None diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml new file mode 100644 index 0000000..ca6e2f9 --- /dev/null +++ b/config/manager/manager.yaml @@ -0,0 +1,86 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: microshift-installer-system +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: microshift-installer-controller-manager + namespace: microshift-installer-system + labels: + control-plane: controller-manager +spec: + selector: + matchLabels: + control-plane: controller-manager + replicas: 1 + template: + metadata: + labels: + control-plane: controller-manager + spec: + serviceAccountName: microshift-installer-controller-manager + containers: + - name: manager + image: microshift-installer:latest + imagePullPolicy: IfNotPresent + command: + - /manager + args: + - --leader-elect=false + ports: + - name: metrics + containerPort: 8080 + protocol: TCP + - name: healthz + containerPort: 8081 + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + env: + # AWS credentials are picked up from environment variables + # Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY here if needed + # - name: AWS_ACCESS_KEY_ID + # valueFrom: + # secretKeyRef: + # name: aws-credentials + # key: access-key-id + # - name: AWS_SECRET_ACCESS_KEY + # valueFrom: + # secretKeyRef: + # name: aws-credentials + # key: secret-access-key + # - name: AWS_REGION + # value: us-east-2 + # + # GCP credentials - set GOOGLE_APPLICATION_CREDENTIALS to the + # path of the mounted service account key. + # - name: GOOGLE_APPLICATION_CREDENTIALS + # value: /etc/gcp/sa-key.json + # volumeMounts: + # - name: gcp-sa-key + # mountPath: /etc/gcp + # readOnly: true + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + capabilities: + drop: + - ALL + # volumes: + # - name: gcp-sa-key + # secret: + # secretName: + terminationGracePeriodSeconds: 10 diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml new file mode 100644 index 0000000..088b6fb --- /dev/null +++ b/config/rbac/role.yaml @@ -0,0 +1,96 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: microshift-installer-manager-role +rules: + - apiGroups: + - cluster-install.io + resources: + - microshiftdeployments + - microshiftdeployments/status + - microshiftdeployments/finalizers + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - cluster-install.io + resources: + - microshiftprovisioners + - microshiftprovisioners/status + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - cluster-install.io + resources: + - microshiftdeprovisioners + - microshiftdeprovisioners/status + verbs: + - get + - list + - watch + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: microshift-installer-metrics-reader +rules: + - nonResourceURLs: + - /metrics + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: microshift-installer-proxy-role +rules: + - apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create + - apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create diff --git a/config/rbac/role_binding.yaml b/config/rbac/role_binding.yaml new file mode 100644 index 0000000..afca093 --- /dev/null +++ b/config/rbac/role_binding.yaml @@ -0,0 +1,25 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: microshift-installer-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: microshift-installer-manager-role +subjects: + - kind: ServiceAccount + name: microshift-installer-controller-manager + namespace: microshift-installer-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: microshift-installer-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: microshift-installer-proxy-role +subjects: + - kind: ServiceAccount + name: microshift-installer-controller-manager + namespace: microshift-installer-system diff --git a/config/rbac/service_account.yaml b/config/rbac/service_account.yaml new file mode 100644 index 0000000..5c50bf0 --- /dev/null +++ b/config/rbac/service_account.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: microshift-installer-controller-manager + namespace: microshift-installer-system diff --git a/config/samples/install-config.yaml b/config/samples/install-config.yaml new file mode 100644 index 0000000..2540484 --- /dev/null +++ b/config/samples/install-config.yaml @@ -0,0 +1,30 @@ +# Sample ConfigMap containing install-config.yaml +# Create this first, then reference it in MicroShiftDeployment.spec.installConfigRef +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-cluster-install-config + namespace: default +data: + install-config.yaml: | + apiVersion: v1 + metadata: + name: my-cluster + baseDomain: example.com + cloudProvider: aws + ocpVersion: "4.14" + pullSecret: '{"auths":{"cloud.openshift.com":{"auth":""}}}' + sshKey: | + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ... + sshPrivateKeyPath: ~/.ssh/id_rsa + rhsm: + username: your-rhn-username + password: your-rhn-password + platform: + aws: + region: us-east-2 + instanceType: t2.medium + network: + vpcCIDR: 10.0.0.0/16 + publicSubnetCIDR: 10.0.0.0/20 + privateSubnetCIDR: 10.0.16.0/20 diff --git a/config/samples/microshiftdeployment_sample.yaml b/config/samples/microshiftdeployment_sample.yaml new file mode 100644 index 0000000..116cc80 --- /dev/null +++ b/config/samples/microshiftdeployment_sample.yaml @@ -0,0 +1,32 @@ +# Sample MicroShiftDeployment CR +# Prerequisites: +# 1. Create the install-config ConfigMap first: +# kubectl apply -f config/samples/install-config.yaml +# 2. For GCP: create a Secret with your service account key: +# kubectl create secret generic gcp-sa-key \ +# --from-file=service-account.json=/path/to/sa-key.json +# 3. For AWS: ensure the operator pod has AWS_ACCESS_KEY_ID and +# AWS_SECRET_ACCESS_KEY environment variables set. +apiVersion: cluster-install.io/v1alpha1 +kind: MicroShiftDeployment +metadata: + name: my-cluster + namespace: default +spec: + # Cluster name for cloud resource naming (defaults to metadata.name) + clusterName: my-cluster + + # Reference the ConfigMap containing install-config.yaml + installConfigRef: + name: my-cluster-install-config + + # (Optional) Cloud credential references + # credentialsRef: + # gcpServiceAccountKey: + # name: gcp-sa-key + + # Provisioning timeout in seconds (default: 1800) + provisionTimeoutSeconds: 1800 + + # Auto-destroy cloud resources when this CR is deleted (default: true) + autoDestroy: true diff --git a/controllers/microshiftdeployment_controller.go b/controllers/microshiftdeployment_controller.go new file mode 100644 index 0000000..4a65c3d --- /dev/null +++ b/controllers/microshiftdeployment_controller.go @@ -0,0 +1,351 @@ +package controllers + +import ( + "context" + "fmt" + "strings" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + 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" + + v1alpha1 "github.com/hchenxa/microshift-installer/api/v1alpha1" +) + +// MicroShiftDeploymentReconciler reconciles MicroShiftDeployment resources. +type MicroShiftDeploymentReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=cluster-install.io,resources=microshiftdeployments,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=cluster-install.io,resources=microshiftdeployments/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=cluster-install.io,resources=microshiftdeployments/finalizers,verbs=update +// +kubebuilder:rbac:groups=cluster-install.io,resources=microshiftprovisioners,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=cluster-install.io,resources=microshiftprovisioners/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=cluster-install.io,resources=microshiftdeprovisioners,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=cluster-install.io,resources=microshiftdeprovisioners/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch + +func (r *MicroShiftDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + logger.V(1).Info("Reconciling MicroShiftDeployment", "name", req.NamespacedName) + + var deploy v1alpha1.MicroShiftDeployment + if err := r.Get(ctx, req.NamespacedName, &deploy); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Handle deletion — finalizer-based teardown + if !deploy.DeletionTimestamp.IsZero() { + return r.reconcileDelete(ctx, &deploy) + } + + // Ensure finalizer is present + if !controllerutil.ContainsFinalizer(&deploy, v1alpha1.MicroShiftDeploymentFinalizer) { + controllerutil.AddFinalizer(&deploy, v1alpha1.MicroShiftDeploymentFinalizer) + if err := r.Update(ctx, &deploy); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + + // Handle normal lifecycle + return r.reconcileNormal(ctx, &deploy) +} + +// reconcileNormal handles the create/update lifecycle. +func (r *MicroShiftDeploymentReconciler) reconcileNormal(ctx context.Context, deploy *v1alpha1.MicroShiftDeployment) (ctrl.Result, error) { + switch deploy.Status.Phase { + case "", v1alpha1.DeploymentPhasePending: + return r.startProvisioning(ctx, deploy) + + case v1alpha1.DeploymentPhaseProvisioning: + return r.checkProvisionerStatus(ctx, deploy) + + case v1alpha1.DeploymentPhaseReady: + return ctrl.Result{}, nil + + case v1alpha1.DeploymentPhaseFailed: + // If spec generation changed, retry + if deploy.Generation != deploy.Status.ObservedGeneration { + deploy.Status.Phase = v1alpha1.DeploymentPhasePending + deploy.Status.ObservedGeneration = deploy.Generation + _ = r.Status().Update(ctx, deploy) + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, nil + } + + return ctrl.Result{}, nil +} + +// startProvisioning creates a MicroShiftProvisioner CR with Operation=Create. +func (r *MicroShiftDeploymentReconciler) startProvisioning(ctx context.Context, deploy *v1alpha1.MicroShiftDeployment) (ctrl.Result, error) { + logger := log.FromContext(ctx) + logger.Info("Starting provisioning", "deployment", deploy.Name) + + installConfigYAML, err := r.readInstallConfig(ctx, deploy) + if err != nil { + r.setDeploymentCondition(deploy, metav1.ConditionFalse, "ConfigReadFailed", err.Error()) + deploy.Status.Phase = v1alpha1.DeploymentPhaseFailed + _ = r.Status().Update(ctx, deploy) + return ctrl.Result{}, err + } + + clusterName := deploy.Spec.ClusterName + if clusterName == "" { + clusterName = deploy.Name + } + + cloudProvider := extractCloudProvider(installConfigYAML) + + prov := &v1alpha1.MicroShiftProvisioner{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-provisioner", clusterName), + Namespace: deploy.Namespace, + }, + Spec: v1alpha1.MicroShiftProvisionerSpec{ + ClusterName: clusterName, + InstallConfig: installConfigYAML, + CloudProvider: cloudProvider, + CredentialsRef: cloneCredentialsRef(deploy.Spec.CredentialsRef), + }, + } + + if err := controllerutil.SetOwnerReference(deploy, prov, r.Scheme); err != nil { + return ctrl.Result{}, err + } + + if err := r.Create(ctx, prov); err != nil { + if !apierrors.IsAlreadyExists(err) { + return ctrl.Result{}, err + } + // Already exists — fetch latest + _ = r.Get(ctx, client.ObjectKeyFromObject(prov), prov) + } + + deploy.Status.Phase = v1alpha1.DeploymentPhaseProvisioning + deploy.Status.ProvisionerRef = &corev1.ObjectReference{ + APIVersion: v1alpha1.GroupVersion.String(), + Kind: "MicroShiftProvisioner", + Name: prov.Name, + Namespace: prov.Namespace, + UID: prov.UID, + } + r.setDeploymentCondition(deploy, metav1.ConditionFalse, "ProvisionerCreated", + fmt.Sprintf("Provisioner %s created", prov.Name)) + _ = r.Status().Update(ctx, deploy) + + return ctrl.Result{RequeueAfter: 5 * time.Second}, nil +} + +// checkProvisionerStatus syncs the provisioner's status back to the deployment. +func (r *MicroShiftDeploymentReconciler) checkProvisionerStatus(ctx context.Context, deploy *v1alpha1.MicroShiftDeployment) (ctrl.Result, error) { + if deploy.Status.ProvisionerRef == nil { + return ctrl.Result{}, nil + } + + var prov v1alpha1.MicroShiftProvisioner + if err := r.Get(ctx, types.NamespacedName{ + Name: deploy.Status.ProvisionerRef.Name, + Namespace: deploy.Status.ProvisionerRef.Namespace, + }, &prov); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + switch prov.Status.Phase { + case v1alpha1.ProvisionerPhaseSucceeded: + deploy.Status.Phase = v1alpha1.DeploymentPhaseReady + deploy.Status.ClusterInfo = prov.Status.ClusterInfo + deploy.Status.Resources = prov.Status.Resources + deploy.Status.ObservedGeneration = deploy.Generation + r.setDeploymentCondition(deploy, metav1.ConditionTrue, "ProvisioningSucceeded", + "Cluster provisioning completed successfully") + return ctrl.Result{}, r.Status().Update(ctx, deploy) + + case v1alpha1.ProvisionerPhaseFailed: + msg := getConditionMessage(prov.Status.Conditions) + deploy.Status.Phase = v1alpha1.DeploymentPhaseFailed + r.setDeploymentCondition(deploy, metav1.ConditionFalse, "ProvisioningFailed", msg) + _ = r.Status().Update(ctx, deploy) + return ctrl.Result{}, nil + + default: + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + } +} + +// reconcileDelete handles deletion with finalizer-based auto-destroy. +func (r *MicroShiftDeploymentReconciler) reconcileDelete(ctx context.Context, deploy *v1alpha1.MicroShiftDeployment) (ctrl.Result, error) { + logger := log.FromContext(ctx) + + autoDestroy := true + if deploy.Spec.AutoDestroy != nil { + autoDestroy = *deploy.Spec.AutoDestroy + } + if !autoDestroy { + controllerutil.RemoveFinalizer(deploy, v1alpha1.MicroShiftDeploymentFinalizer) + return ctrl.Result{}, r.Update(ctx, deploy) + } + + clusterName := deploy.Spec.ClusterName + if clusterName == "" { + clusterName = deploy.Name + } + deprovName := fmt.Sprintf("%s-deprovisioner", clusterName) + + // Check if a deprovisioner already exists + var existingDeprov v1alpha1.MicroShiftDeprovisioner + if err := r.Get(ctx, types.NamespacedName{Name: deprovName, Namespace: deploy.Namespace}, &existingDeprov); err == nil { + switch existingDeprov.Status.Phase { + case v1alpha1.DeprovisionerPhaseSucceeded: + controllerutil.RemoveFinalizer(deploy, v1alpha1.MicroShiftDeploymentFinalizer) + return ctrl.Result{}, r.Update(ctx, deploy) + case v1alpha1.DeprovisionerPhaseFailed: + logger.Info("Deprovision failed, removing finalizer (manual cleanup may be needed)") + controllerutil.RemoveFinalizer(deploy, v1alpha1.MicroShiftDeploymentFinalizer) + return ctrl.Result{}, r.Update(ctx, deploy) + default: + deploy.Status.Phase = v1alpha1.DeploymentPhaseDeleting + _ = r.Status().Update(ctx, deploy) + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + } + } + + // Create a deprovisioner + logger.Info("Creating deprovisioner", "deployment", deploy.Name) + + installConfigYAML, err := r.readInstallConfig(ctx, deploy) + if err != nil { + installConfigYAML = "" + } + + cloudProvider := extractCloudProvider(installConfigYAML) + if cloudProvider == "" { + cloudProvider = detectProviderFromResources(deploy.Status.Resources) + } + if cloudProvider == "" { + logger.Info("Cannot determine cloud provider, removing finalizer") + controllerutil.RemoveFinalizer(deploy, v1alpha1.MicroShiftDeploymentFinalizer) + return ctrl.Result{}, r.Update(ctx, deploy) + } + + deprov := &v1alpha1.MicroShiftDeprovisioner{ + ObjectMeta: metav1.ObjectMeta{ + Name: deprovName, + Namespace: deploy.Namespace, + }, + Spec: v1alpha1.MicroShiftDeprovisionerSpec{ + ClusterName: clusterName, + CloudProvider: cloudProvider, + InstallConfig: installConfigYAML, + Resources: deploy.Status.Resources, + CredentialsRef: cloneCredentialsRef(deploy.Spec.CredentialsRef), + }, + } + + if err := r.Create(ctx, deprov); err != nil && !apierrors.IsAlreadyExists(err) { + return ctrl.Result{}, err + } + + deploy.Status.Phase = v1alpha1.DeploymentPhaseDeleting + _ = r.Status().Update(ctx, deploy) + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil +} + +// ── Helpers ────────────────────────────────────────────────────────────────── + +func (r *MicroShiftDeploymentReconciler) readInstallConfig(ctx context.Context, deploy *v1alpha1.MicroShiftDeployment) (string, error) { + if deploy.Spec.InstallConfigRef == nil { + return "", fmt.Errorf("installConfigRef is not set") + } + var cm corev1.ConfigMap + if err := r.Get(ctx, types.NamespacedName{ + Name: deploy.Spec.InstallConfigRef.Name, + Namespace: deploy.Namespace, + }, &cm); err != nil { + return "", fmt.Errorf("reading ConfigMap %s: %w", deploy.Spec.InstallConfigRef.Name, err) + } + yamlData, ok := cm.Data["install-config.yaml"] + if !ok || yamlData == "" { + return "", fmt.Errorf("ConfigMap %s missing key install-config.yaml", deploy.Spec.InstallConfigRef.Name) + } + return yamlData, nil +} + +func (r *MicroShiftDeploymentReconciler) setDeploymentCondition(deploy *v1alpha1.MicroShiftDeployment, status metav1.ConditionStatus, reason, message string) { + now := metav1.Now() + for i, c := range deploy.Status.Conditions { + if c.Type == "Ready" { + deploy.Status.Conditions[i].Status = status + deploy.Status.Conditions[i].Reason = reason + deploy.Status.Conditions[i].Message = message + deploy.Status.Conditions[i].LastTransitionTime = now + return + } + } + deploy.Status.Conditions = append(deploy.Status.Conditions, metav1.Condition{ + Type: "Ready", + Status: status, + LastTransitionTime: now, + Reason: reason, + Message: message, + }) +} + +func getConditionMessage(conditions []metav1.Condition) string { + for _, c := range conditions { + if (c.Type == "Completed" || c.Type == "Ready") && c.Status == metav1.ConditionFalse { + return c.Message + } + } + return "unknown error" +} + +func extractCloudProvider(yamlData string) string { + lines := strings.Split(yamlData, "\n") + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "cloudProvider:") { + val := strings.TrimSpace(strings.TrimPrefix(trimmed, "cloudProvider:")) + val = strings.Trim(val, `"'`) + if val == "aws" || val == "gcp" { + return val + } + } + } + return "" +} + +func detectProviderFromResources(resources []v1alpha1.ResourceRecord) string { + for _, r := range resources { + if r.Provider == "aws" || r.Provider == "gcp" { + return r.Provider + } + } + return "" +} + +func cloneCredentialsRef(ref *v1alpha1.CredentialsRef) *v1alpha1.CredentialsRef { + if ref == nil { + return nil + } + return ref.DeepCopy() +} + +func (r *MicroShiftDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.MicroShiftDeployment{}). + Owns(&v1alpha1.MicroShiftProvisioner{}). + Complete(r) +} diff --git a/controllers/microshiftdeprovisioner_controller.go b/controllers/microshiftdeprovisioner_controller.go new file mode 100644 index 0000000..107c493 --- /dev/null +++ b/controllers/microshiftdeprovisioner_controller.go @@ -0,0 +1,220 @@ +package controllers + +import ( + "context" + "fmt" + "sync" + "time" + + "gopkg.in/yaml.v3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + k8stypes "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + v1alpha1 "github.com/hchenxa/microshift-installer/api/v1alpha1" + "github.com/hchenxa/microshift-installer/pkg/platform" + "github.com/hchenxa/microshift-installer/pkg/state" + coretypes "github.com/hchenxa/microshift-installer/pkg/types" +) + +// MicroShiftDeprovisionerReconciler reconciles MicroShiftDeprovisioner resources. +type MicroShiftDeprovisionerReconciler struct { + client.Client + Scheme *runtime.Scheme + + mu sync.Mutex + running map[k8stypes.NamespacedName]context.CancelFunc +} + +// +kubebuilder:rbac:groups=cluster-install.io,resources=microshiftdeprovisioners,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=cluster-install.io,resources=microshiftdeprovisioners/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch + +func (r *MicroShiftDeprovisionerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + logger.V(1).Info("Reconciling MicroShiftDeprovisioner", "name", req.NamespacedName) + + var deprov v1alpha1.MicroShiftDeprovisioner + if err := r.Get(ctx, req.NamespacedName, &deprov); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + switch deprov.Status.Phase { + case "", v1alpha1.DeprovisionerPhasePending: + return r.startDeprovision(ctx, &deprov) + case v1alpha1.DeprovisionerPhaseRunning: + return r.checkDeprovision(ctx, &deprov) + case v1alpha1.DeprovisionerPhaseSucceeded, v1alpha1.DeprovisionerPhaseFailed: + return ctrl.Result{}, nil + } + + return ctrl.Result{}, nil +} + +func (r *MicroShiftDeprovisionerReconciler) startDeprovision(ctx context.Context, deprov *v1alpha1.MicroShiftDeprovisioner) (ctrl.Result, error) { + logger := log.FromContext(ctx) + logger.Info("Starting deprovision", "deprovisioner", deprov.Name, "cluster", deprov.Spec.ClusterName) + + deprov.Status.Phase = v1alpha1.DeprovisionerPhaseRunning + r.setDeprovisionerCondition(deprov, metav1.ConditionFalse, "Running", "Deprovision in progress") + if err := r.Status().Update(ctx, deprov); err != nil { + return ctrl.Result{}, err + } + + nn := k8stypes.NamespacedName{Name: deprov.Name, Namespace: deprov.Namespace} + wCtx, cancel := context.WithCancel(context.Background()) + + r.mu.Lock() + if oldCancel, ok := r.running[nn]; ok { + oldCancel() + } + if r.running == nil { + r.running = make(map[k8stypes.NamespacedName]context.CancelFunc) + } + r.running[nn] = cancel + r.mu.Unlock() + + go r.runDestroy(wCtx, nn) + + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil +} + +func (r *MicroShiftDeprovisionerReconciler) checkDeprovision(ctx context.Context, deprov *v1alpha1.MicroShiftDeprovisioner) (ctrl.Result, error) { + nn := k8stypes.NamespacedName{Name: deprov.Name, Namespace: deprov.Namespace} + + r.mu.Lock() + _, running := r.running[nn] + r.mu.Unlock() + + if running { + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + } + + logger := log.FromContext(ctx) + logger.Info("No running deprovision found, restarting", "deprovisioner", deprov.Name) + + deprov.Status.Phase = v1alpha1.DeprovisionerPhasePending + _ = r.Status().Update(ctx, deprov) + return ctrl.Result{Requeue: true}, nil +} + +func (r *MicroShiftDeprovisionerReconciler) runDestroy(ctx context.Context, nn k8stypes.NamespacedName) { + logger := log.FromContext(ctx) + logger.Info("Deprovision worker started", "deprovisioner", nn.Name) + + defer func() { + r.mu.Lock() + delete(r.running, nn) + r.mu.Unlock() + }() + + var deprov v1alpha1.MicroShiftDeprovisioner + if err := r.Get(ctx, nn, &deprov); err != nil { + logger.Error(err, "Failed to get deprovisioner") + return + } + + // 1. Build state from resources + st := &state.State{} + for _, res := range deprov.Spec.Resources { + st.Add(state.Resource{ + Name: res.Name, + Type: res.Type, + Provider: res.Provider, + ID: res.ID, + }) + } + + // 2. Build minimal install config + cfg := &coretypes.InstallConfig{ + CloudProvider: deprov.Spec.CloudProvider, + Metadata: coretypes.Metadata{ + Name: deprov.Spec.ClusterName, + }, + Platform: getPlatformDefaultsForDeprov(deprov.Spec.CloudProvider), + } + + // 3. Parse install config for cloud-specific settings + if deprov.Spec.InstallConfig != "" { + var parsed coretypes.InstallConfig + if err := yaml.Unmarshal([]byte(deprov.Spec.InstallConfig), &parsed); err == nil { + if parsed.Platform.AWS != nil && parsed.Platform.AWS.Region != "" { + cfg.Platform.AWS = parsed.Platform.AWS + } + if parsed.Platform.GCP != nil && parsed.Platform.GCP.Project != "" { + cfg.Platform.GCP = parsed.Platform.GCP + } + if parsed.BaseDomain != "" { + cfg.BaseDomain = parsed.BaseDomain + } + } + } + + // 4. Create platform + plat := platform.New(cfg) + if plat == nil { + r.setDeprovFailed(ctx, &deprov, fmt.Sprintf("unsupported cloud provider: %s", cfg.CloudProvider)) + return + } + + // 5. Run destruction + if err := plat.Destroy(ctx, st); err != nil { + r.setDeprovFailed(ctx, &deprov, fmt.Sprintf("destruction failed: %s", err)) + return + } + + // 6. Update status + deprov.Status.Phase = v1alpha1.DeprovisionerPhaseSucceeded + r.setDeprovisionerCondition(&deprov, metav1.ConditionTrue, "Succeeded", + "Cluster destruction completed") + if err := r.Status().Update(ctx, &deprov); err != nil { + logger.Error(err, "Failed to update deprovisioner status") + } + logger.Info("Deprovision completed successfully", "cluster", deprov.Spec.ClusterName) +} + +func (r *MicroShiftDeprovisionerReconciler) setDeprovFailed(ctx context.Context, deprov *v1alpha1.MicroShiftDeprovisioner, msg string) { + logger := log.FromContext(ctx) + logger.Error(nil, "Deprovision failed", "deprovisioner", deprov.Name, "error", msg) + deprov.Status.Phase = v1alpha1.DeprovisionerPhaseFailed + r.setDeprovisionerCondition(deprov, metav1.ConditionFalse, "Failed", msg) + _ = r.Status().Update(ctx, deprov) +} + +func (r *MicroShiftDeprovisionerReconciler) setDeprovisionerCondition(deprov *v1alpha1.MicroShiftDeprovisioner, status metav1.ConditionStatus, reason, message string) { + now := metav1.Now() + for i, c := range deprov.Status.Conditions { + if c.Type == "Completed" { + deprov.Status.Conditions[i].Status = status + deprov.Status.Conditions[i].Reason = reason + deprov.Status.Conditions[i].Message = message + deprov.Status.Conditions[i].LastTransitionTime = now + return + } + } + deprov.Status.Conditions = append(deprov.Status.Conditions, metav1.Condition{ + Type: "Completed", Status: status, + LastTransitionTime: now, Reason: reason, Message: message, + }) +} + +func getPlatformDefaultsForDeprov(cloudProvider string) coretypes.Platform { + switch cloudProvider { + case "aws": + return coretypes.Platform{AWS: &coretypes.AWSPlatform{Region: "us-east-2"}} + case "gcp": + return coretypes.Platform{GCP: &coretypes.GCPPlatform{ + Project: "default", Region: "us-central1", Zone: "us-central1-a", + }} + } + return coretypes.Platform{} +} + +func (r *MicroShiftDeprovisionerReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.MicroShiftDeprovisioner{}). + Complete(r) +} diff --git a/controllers/microshiftprovisioner_controller.go b/controllers/microshiftprovisioner_controller.go new file mode 100644 index 0000000..ceb1a65 --- /dev/null +++ b/controllers/microshiftprovisioner_controller.go @@ -0,0 +1,204 @@ +package controllers + +import ( + "context" + "fmt" + "sync" + "time" + + "gopkg.in/yaml.v3" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + k8stypes "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + v1alpha1 "github.com/hchenxa/microshift-installer/api/v1alpha1" + "github.com/hchenxa/microshift-installer/pkg/platform" + "github.com/hchenxa/microshift-installer/pkg/state" + coretypes "github.com/hchenxa/microshift-installer/pkg/types" +) + +// MicroShiftProvisionerReconciler reconciles MicroShiftProvisioner resources. +type MicroShiftProvisionerReconciler struct { + client.Client + Scheme *runtime.Scheme + + mu sync.Mutex + running map[k8stypes.NamespacedName]context.CancelFunc +} + +// +kubebuilder:rbac:groups=cluster-install.io,resources=microshiftprovisioners,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=cluster-install.io,resources=microshiftprovisioners/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch + +func (r *MicroShiftProvisionerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := log.FromContext(ctx) + logger.V(1).Info("Reconciling MicroShiftProvisioner", "name", req.NamespacedName) + + var prov v1alpha1.MicroShiftProvisioner + if err := r.Get(ctx, req.NamespacedName, &prov); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + switch prov.Status.Phase { + case "", v1alpha1.ProvisionerPhasePending: + return r.startProvision(ctx, &prov) + case v1alpha1.ProvisionerPhaseRunning: + return r.checkProvision(ctx, &prov) + case v1alpha1.ProvisionerPhaseSucceeded, v1alpha1.ProvisionerPhaseFailed: + return ctrl.Result{}, nil + } + + return ctrl.Result{}, nil +} + +func (r *MicroShiftProvisionerReconciler) startProvision(ctx context.Context, prov *v1alpha1.MicroShiftProvisioner) (ctrl.Result, error) { + logger := log.FromContext(ctx) + logger.Info("Starting provision", "provisioner", prov.Name, "cluster", prov.Spec.ClusterName) + + prov.Status.Phase = v1alpha1.ProvisionerPhaseRunning + prov.Status.ObservedGeneration = prov.Generation + r.setProvisionerCondition(prov, metav1.ConditionFalse, "Running", "Provision in progress") + if err := r.Status().Update(ctx, prov); err != nil { + return ctrl.Result{}, err + } + + nn := k8stypes.NamespacedName{Name: prov.Name, Namespace: prov.Namespace} + wCtx, cancel := context.WithCancel(context.Background()) + + r.mu.Lock() + if oldCancel, ok := r.running[nn]; ok { + oldCancel() + } + if r.running == nil { + r.running = make(map[k8stypes.NamespacedName]context.CancelFunc) + } + r.running[nn] = cancel + r.mu.Unlock() + + go r.runCreate(wCtx, nn) + + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil +} + +func (r *MicroShiftProvisionerReconciler) checkProvision(ctx context.Context, prov *v1alpha1.MicroShiftProvisioner) (ctrl.Result, error) { + nn := k8stypes.NamespacedName{Name: prov.Name, Namespace: prov.Namespace} + + r.mu.Lock() + _, running := r.running[nn] + r.mu.Unlock() + + if running { + return ctrl.Result{RequeueAfter: 10 * time.Second}, nil + } + + logger := log.FromContext(ctx) + logger.Info("No running provision found, restarting", "provisioner", prov.Name) + prov.Status.Phase = v1alpha1.ProvisionerPhasePending + _ = r.Status().Update(ctx, prov) + return ctrl.Result{Requeue: true}, nil +} + +func (r *MicroShiftProvisionerReconciler) runCreate(ctx context.Context, nn k8stypes.NamespacedName) { + logger := log.FromContext(ctx) + logger.Info("Provision worker started", "provisioner", nn.Name) + + defer func() { + r.mu.Lock() + delete(r.running, nn) + r.mu.Unlock() + }() + + var prov v1alpha1.MicroShiftProvisioner + if err := r.Get(ctx, nn, &prov); err != nil { + logger.Error(err, "Failed to get provisioner") + return + } + + // 1. Parse install config + cfg := &coretypes.InstallConfig{} + if err := yaml.Unmarshal([]byte(prov.Spec.InstallConfig), cfg); err != nil { + r.setProvisionFailed(ctx, &prov, fmt.Sprintf("parsing install config: %s", err)) + return + } + if cfg.Metadata.Name == "" { + cfg.Metadata.Name = prov.Spec.ClusterName + } + if err := cfg.Validate(); err != nil { + r.setProvisionFailed(ctx, &prov, fmt.Sprintf("invalid install config: %s", err)) + return + } + + // 2. Create platform and run provisioning + plat := platform.New(cfg) + if plat == nil { + r.setProvisionFailed(ctx, &prov, fmt.Sprintf("unsupported cloud provider: %s", cfg.CloudProvider)) + return + } + + st := &state.State{} + info, err := plat.Create(ctx, cfg, st) + if err != nil { + r.setProvisionFailed(ctx, &prov, fmt.Sprintf("provisioning failed: %s", err)) + return + } + + // 3. Update status + prov.Status.Phase = v1alpha1.ProvisionerPhaseSucceeded + prov.Status.ClusterInfo = &v1alpha1.ClusterInfo{ + NodePublicIP: info.NodePublicIP, + SSHCommand: info.SSHCommand, + KubeconfigCommand: info.KubeconfigCmd, + DNSFQDN: info.DNSFQDN, + } + prov.Status.Resources = convertResources(st.All()) + r.setProvisionerCondition(&prov, metav1.ConditionTrue, "Succeeded", + "Cluster provisioning completed") + if err := r.Status().Update(ctx, &prov); err != nil { + logger.Error(err, "Failed to update provisioner status") + } + logger.Info("Provision completed successfully", "cluster", prov.Spec.ClusterName) +} + +func (r *MicroShiftProvisionerReconciler) setProvisionFailed(ctx context.Context, prov *v1alpha1.MicroShiftProvisioner, msg string) { + logger := log.FromContext(ctx) + logger.Error(nil, "Provision failed", "provisioner", prov.Name, "error", msg) + prov.Status.Phase = v1alpha1.ProvisionerPhaseFailed + r.setProvisionerCondition(prov, metav1.ConditionFalse, "Failed", msg) + _ = r.Status().Update(ctx, prov) +} + +func (r *MicroShiftProvisionerReconciler) setProvisionerCondition(prov *v1alpha1.MicroShiftProvisioner, status metav1.ConditionStatus, reason, message string) { + now := metav1.Now() + for i, c := range prov.Status.Conditions { + if c.Type == "Completed" { + prov.Status.Conditions[i].Status = status + prov.Status.Conditions[i].Reason = reason + prov.Status.Conditions[i].Message = message + prov.Status.Conditions[i].LastTransitionTime = now + return + } + } + prov.Status.Conditions = append(prov.Status.Conditions, metav1.Condition{ + Type: "Completed", Status: status, + LastTransitionTime: now, Reason: reason, Message: message, + }) +} + +func convertResources(stResources []state.Resource) []v1alpha1.ResourceRecord { + out := make([]v1alpha1.ResourceRecord, len(stResources)) + for i, r := range stResources { + out[i] = v1alpha1.ResourceRecord{ + Name: r.Name, Type: r.Type, Provider: r.Provider, ID: r.ID, + } + } + return out +} + +func (r *MicroShiftProvisionerReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.MicroShiftProvisioner{}). + Complete(r) +} diff --git a/go.mod b/go.mod index e150dff..e2e605a 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,12 @@ require ( github.com/aws/aws-sdk-go-v2/service/route53 v1.63.3 github.com/spf13/cobra v1.10.2 google.golang.org/api v0.286.0 - google.golang.org/protobuf v1.36.11 + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.36.2 + k8s.io/apimachinery v0.36.2 + k8s.io/client-go v0.36.2 + sigs.k8s.io/controller-runtime v0.24.1 ) require ( @@ -31,33 +35,73 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect github.com/aws/smithy-go v1.27.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.16 // indirect github.com/googleapis/gax-go/v2 v2.22.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.2 // indirect github.com/mattn/go-isatty v0.0.8 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/spf13/pflag v1.0.9 // indirect + github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.53.0 // indirect golang.org/x/net v0.56.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/term v0.44.0 // indirect golang.org/x/text v0.38.0 // indirect + golang.org/x/time v0.15.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260511170946-3700d4141b60 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect google.golang.org/grpc v1.81.1 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + k8s.io/apiextensions-apiserver v0.36.0 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index fa2be79..dfe8351 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= @@ -44,25 +46,58 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUY github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc= github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8= github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -75,20 +110,56 @@ github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +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 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.27.4 h1:fcEcQW/A++6aZAZQNUmNjvA9PSOzefMJBerHJ4t8v8Y= +github.com/onsi/ginkgo/v2 v2.27.4/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q= +github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -97,9 +168,19 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= @@ -115,12 +196,23 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -150,10 +242,16 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.286.0 h1:TdTXMvzYKnWV1/lPbCdbXRqBrkDqjPto22H2xeZZ8LI= @@ -166,11 +264,39 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY= +k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg= +k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0= +k8s.io/apiextensions-apiserver v0.36.0/go.mod h1:kGDjH0msuiIB3tgsYRV0kS9GqpMYMUsQ3GHv7TApyug= +k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ= +k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4= +k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI= +k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4= +sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/pkg/state/state.go b/pkg/state/state.go index 2e66814..c58a62f 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -43,9 +43,13 @@ func Load(path string) (*State, error) { } // Save persists the state to its file path. +// If the state was created in-memory (no path), Save is a no-op. func (s *State) Save() error { s.mu.Lock() defer s.mu.Unlock() + if s.path == "" { + return nil // in-memory only + } data, err := json.MarshalIndent(s, "", " ") if err != nil { return fmt.Errorf("marshalling state: %w", err)