Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Lint

on:
push:
pull_request:

permissions:
contents: read

jobs:
lint:
permissions:
contents: read # for actions/checkout to fetch code
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
name: Run on Ubuntu
runs-on: ubuntu-latest
steps:
- name: Clone the code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

- name: Setup Go
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version-file: go.mod

- name: Run linter
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
version: v2.10.1
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*.dll
*.so
*.dylib
bin/*

# Test binary, built with `go test -c`
*.test
Expand Down
136 changes: 113 additions & 23 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,28 +1,118 @@
# Version is set from the latest git tag or commit hash
VERSION=$(shell git describe --tags --always)

.PHONY: generate
# generate code
generate:
buf generate
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif

##@ General

# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk command is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI control characters for terminal formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php

.PHONY: help
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

##@ Development

.PHONY: proto
proto: buf ## Generate code from proto files
"$(BUF)" generate
rm openapi.pb.go ts/src/openapi_pb.* # workaround for buf exclude

Copilot AI Feb 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proto target deletes generated files using rm without -f. If buf generation output changes (or the files are already absent), this will make make proto fail even though generation succeeded. Use rm -f (or guard with test -e) to keep the target idempotent.

Suggested change
rm openapi.pb.go ts/src/openapi_pb.* # workaround for buf exclude
rm -f openapi.pb.go ts/src/openapi_pb.* # workaround for buf exclude

Copilot uses AI. Check for mistakes.

.PHONY: manifests
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
"$(CONTROLLER_GEN)" rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases

.PHONY: generate
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" paths="./..."

.PHONY: fmt
fmt: ## Run go fmt against code.
go fmt ./...

.PHONY: vet
vet: ## Run go vet against code.
go vet ./...

.PHONY: tidy
tidy: ## Run go mod tidy
go mod tidy

.PHONY: help
# show help
help:
@echo ''
@echo 'Usage:'
@echo ' make [target]'
@echo ''
@echo 'Targets:'
@awk '/^[a-zA-Z\-0-9]+:/ { \
helpMessage = match(lastLine, /^# (.*)/); \
if (helpMessage) { \
helpCommand = substr($$1, 0, index($$1, ":")-1); \
helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
printf "\033[36m%-22s\033[0m %s\n", helpCommand,helpMessage; \
} \
} \
{ lastLine = $$0 }' $(MAKEFILE_LIST)

.DEFAULT_GOAL := help
.PHONY: lint
lint: golangci-lint ## Run golangci-lint linter
"$(GOLANGCI_LINT)" run

.PHONY: lint-fix
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
"$(GOLANGCI_LINT)" run --fix

.PHONY: lint-config
lint-config: golangci-lint ## Verify golangci-lint linter configuration
"$(GOLANGCI_LINT)" config verify

.PHONY: all
all: proto manifests generate fmt vet tidy lint ## Generate all code from proto files, manifests, and go code.

.DEFAULT_GOAL := all

##@ Dependencies

## Location to install dependencies to
LOCALBIN ?= $(shell pwd)/bin
$(LOCALBIN):
mkdir -p "$(LOCALBIN)"

## Tool Binaries
BUF ?= $(LOCALBIN)/buf
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint

## Tool Versions
BUF_VERSION ?= v1.66.0
CONTROLLER_TOOLS_VERSION ?= v0.20.1
GOLANGCI_LINT_VERSION ?= v2.10.1

.PHONY: buf
buf: $(BUF) ## Download buf locally if necessary.
$(BUF): $(LOCALBIN)
$(call go-install-tool,$(BUF),github.com/bufbuild/buf/cmd/buf,$(BUF_VERSION))

.PHONY: controller-gen
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
$(CONTROLLER_GEN): $(LOCALBIN)
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION))

.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))

# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
# $1 - target path with name of binary
# $2 - package url which can be installed
# $3 - specific version of package
define go-install-tool
@[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \
set -e; \
package=$(2)@$(3) ;\
echo "Downloading $${package}" ;\
rm -f "$(1)" ;\
GOBIN="$(LOCALBIN)" go install $${package} ;\
mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\
} ;\
ln -sf "$$(realpath "$(1)-$(3)")" "$(1)"
endef
48 changes: 48 additions & 0 deletions addons/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
Copyright 2026 The OtterScale Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package v1alpha1 contains API Schema definitions for the addons v1alpha1 API group.
// +kubebuilder:object:generate=true
// +groupName=addons.otterscale.io
package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)

var (
// GroupVersion is group version used to register these objects.
GroupVersion = schema.GroupVersion{Group: "addons.otterscale.io", Version: "v1alpha1"}

// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)

// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)

func addKnownTypes(s *runtime.Scheme) error {
s.AddKnownTypes(GroupVersion,
&Module{},
&ModuleList{},
&ModuleTemplate{},
&ModuleTemplateList{},
)
metav1.AddToGroupVersion(s, GroupVersion)
return nil
}
129 changes: 129 additions & 0 deletions addons/v1alpha1/module_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
Copyright 2026 The OtterScale Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)

// ModuleSpec defines the desired state of an installed Module.
// A Module instantiates a ModuleTemplate by referencing it and optionally
// overriding the target namespace or Helm values.
type ModuleSpec struct {
// TemplateRef is the name of the ModuleTemplate to instantiate.
// This field is immutable after creation.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=253
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="templateRef is immutable"
// +required
TemplateRef string `json:"templateRef"`

// Namespace overrides the default target namespace defined in the ModuleTemplate.
// If not specified, the namespace from the ModuleTemplate is used.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
// +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?)$`
// +optional
Namespace *string `json:"namespace,omitempty"`

// Values overrides the default values for HelmRelease-based modules.
// Only applicable when the referenced ModuleTemplate uses a HelmRelease template.
// Ignored for Kustomization-based modules.
// Accepts arbitrary JSON; RawExtension is used instead of apiextensionsv1.JSON
// to avoid pulling in the k8s.io/apiextensions-apiserver dependency.
// +kubebuilder:pruning:PreserveUnknownFields
// +optional
Values *runtime.RawExtension `json:"values,omitempty"`
}

// ResourceReference is a lightweight reference to a Kubernetes resource managed by the operator.
type ResourceReference struct {
// Name is the name of the referenced resource.
// +required
Name string `json:"name"`

// Namespace is the namespace of the referenced resource.
// +optional
Namespace string `json:"namespace,omitempty"`
}

// ModuleStatus defines the observed state of a Module.
// It contains references to the actual FluxCD resources created by the controller
// and reflects their health status.
type ModuleStatus struct {
// ObservedGeneration is the most recent generation observed by the controller.
// It corresponds to the Module's generation, which is updated on mutation by the API Server.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`

// TemplateGeneration tracks the observed generation of the referenced ModuleTemplate.
// This allows the controller to detect and reconcile template changes.
// +optional
TemplateGeneration int64 `json:"templateGeneration,omitempty"`

// HelmReleaseRef is a reference to the FluxCD HelmRelease managed by this Module.
// +optional
HelmReleaseRef *ResourceReference `json:"helmReleaseRef,omitempty"`

// KustomizationRef is a reference to the FluxCD Kustomization managed by this Module.
// +optional
KustomizationRef *ResourceReference `json:"kustomizationRef,omitempty"`

// Conditions store the status conditions of the Module (e.g., Ready, TemplateNotFound).
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:printcolumn:name="Template",type=string,JSONPath=`.spec.templateRef`
// +kubebuilder:printcolumn:name="Namespace",type=string,JSONPath=`.spec.namespace`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"

// Module is the Schema for the modules API.
// A Module represents an installed platform addon instantiated from a ModuleTemplate.
// The controller creates the corresponding FluxCD HelmRelease or Kustomization
// and reflects its status back to the Module.
type Module struct {
metav1.TypeMeta `json:",inline"`

// Standard object's metadata.
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The JSON tag omitzero is not a standard Go tag for encoding/json. The correct tag to omit a field if it's empty is omitempty. This should be corrected for ObjectMeta, Status, and ListMeta fields in all _types.go files to ensure correct JSON serialization behavior.

Suggested change
metav1.ObjectMeta `json:"metadata,omitzero"`
metav1.ObjectMeta `json:"metadata,omitempty"`


// Spec defines the desired behavior of the Module.
// +required
Spec ModuleSpec `json:"spec"`

// Status represents the current information about the Module.
// +optional
Status ModuleStatus `json:"status,omitzero"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The JSON tag omitzero is not a standard Go tag. Please use omitempty to ensure the field is omitted from JSON output when it is empty.

Suggested change
Status ModuleStatus `json:"status,omitzero"`
Status ModuleStatus `json:"status,omitempty"`

}

// +kubebuilder:object:root=true

// ModuleList contains a list of Module resources.
type ModuleList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The JSON tag omitzero is not a standard Go tag. Please use omitempty to ensure the field is omitted from JSON output when it is empty.

Suggested change
metav1.ListMeta `json:"metadata,omitzero"`
metav1.ListMeta `json:"metadata,omitempty"`

Items []Module `json:"items"`
}
Loading