Skip to content

Commit 7b521e2

Browse files
authored
Add tilt for local development (#120)
1 parent 2e561dd commit 7b521e2

18 files changed

+507
-5
lines changed

.golangci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ issues:
1919
linters:
2020
disable-all: true
2121
enable:
22+
- copyloopvar
2223
- dupl
2324
- errcheck
24-
- exportloopref
2525
- goconst
2626
- gocyclo
2727
- gofmt

Makefile

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ else
1111
GOBIN=$(shell go env GOBIN)
1212
endif
1313

14+
GOARCH := $(shell go env GOARCH)
15+
GOOS := $(shell go env GOOS)
16+
1417
# CONTAINER_TOOL defines the container tool to be used for building images.
1518
# Be aware that the target commands are only tested with Docker which is
1619
# scaffolded by default. However, you might want to replace it to use other
@@ -181,8 +184,12 @@ LOCALBIN ?= $(shell pwd)/bin
181184
$(LOCALBIN):
182185
mkdir -p $(LOCALBIN)
183186

187+
# curl retries
188+
CURL_RETRIES=3
189+
184190
## Tool Binaries
185-
KUBECTL ?= kubectl
191+
KUBECTL ?= $(LOCALBIN)/kubectl-$(ENVTEST_K8S_VERSION)
192+
KUBECTL_BIN ?= $(LOCALBIN)/kubectl
186193
KUSTOMIZE ?= $(LOCALBIN)/kustomize-$(KUSTOMIZE_VERSION)
187194
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen-$(CONTROLLER_TOOLS_VERSION)
188195
ENVTEST ?= $(LOCALBIN)/setup-envtest-$(ENVTEST_VERSION)
@@ -203,6 +210,13 @@ kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
203210
$(KUSTOMIZE): $(LOCALBIN)
204211
$(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION))
205212

213+
.PHONY: kubectl
214+
kubectl: $(KUBECTL) ## Download kubectl locally if necessary.
215+
$(KUBECTL): $(LOCALBIN)
216+
curl --retry $(CURL_RETRIES) -fsL https://dl.k8s.io/release/v$(ENVTEST_K8S_VERSION)/bin/$(GOOS)/$(GOARCH)/kubectl -o $(KUBECTL)
217+
ln -sf "$(KUBECTL)" "$(KUBECTL_BIN)"
218+
chmod +x "$(KUBECTL_BIN)" "$(KUBECTL)"
219+
206220
.PHONY: controller-gen
207221
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
208222
$(CONTROLLER_GEN): $(LOCALBIN)
@@ -241,3 +255,22 @@ GOBIN=$(LOCALBIN) go install $${package} ;\
241255
mv "$$(echo "$(1)" | sed "s/-$(3)$$//")" $(1) ;\
242256
}
243257
endef
258+
259+
## --------------------------------------
260+
## Tilt / Kind
261+
## --------------------------------------
262+
263+
KIND_CLUSTER_NAME ?= metal
264+
265+
.PHONY: kind-create
266+
kind-create: $(ENVTEST) ## create metal kind cluster if needed
267+
./scripts/kind-with-registry.sh
268+
269+
.PHONY: kind-delete
270+
kind-delete: ## Destroys the "metal" kind cluster.
271+
kind delete cluster --name=$(KIND_CLUSTER_NAME)
272+
docker stop kind-registry && docker rm kind-registry
273+
274+
.PHONY: tilt-up
275+
tilt-up: $(ENVTEST) $(KUSTOMIZE) kind-create ## start tilt and build kind cluster if needed
276+
EXP_CLUSTER_RESOURCE_SET=true tilt up

Tiltfile

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env bash
2+
#// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors
3+
#// SPDX-License-Identifier: Apache-2.0
4+
5+
update_settings(k8s_upsert_timeout_secs=60) # on first tilt up, often can take longer than 30 seconds
6+
7+
settings = {
8+
"allowed_contexts": [
9+
"kind-metal"
10+
],
11+
"kubectl": "./bin/kubectl",
12+
"boot_image": "ghcr.io/ironcore-dev/boot-operator:latest",
13+
"cert_manager_version": "v1.15.3",
14+
"new_args": {
15+
"boot": [
16+
"--health-probe-bind-address=:8081",
17+
"--metrics-bind-address=127.0.0.1:8085",
18+
"--leader-elect",
19+
"--default-ipxe-server-url=http://boot-service:30007",
20+
"--default-kernel-url=http://boot-service:30007/image?imageName=ghcr.io/ironcore-dev/os-images/gardenlinux&version=1443.10&layerName=vmlinuz",
21+
"--default-initrd-url=http://boot-service:30007/image?imageName=ghcr.io/ironcore-dev/os-images/gardenlinux&version=1443.10&layerName=initramfs",
22+
"--default-squashfs-url=http://boot-service:30007/image?imageName=ghcr.io/ironcore-dev/os-images/gardenlinux&version=1443.10&layerName=squashfs",
23+
"--ipxe-service-url=http://boot-service:30007",
24+
"--ipxe-service-port=30007",
25+
"--controllers=ipxebootconfig,serverbootconfigpxe,serverbootconfighttp,httpbootconfig",
26+
],
27+
}
28+
}
29+
30+
kubectl = settings.get("kubectl")
31+
32+
if "allowed_contexts" in settings:
33+
allow_k8s_contexts(settings.get("allowed_contexts"))
34+
35+
def deploy_cert_manager():
36+
version = settings.get("cert_manager_version")
37+
print("Installing cert-manager")
38+
local("{} apply -f https://github.com/cert-manager/cert-manager/releases/download/{}/cert-manager.yaml".format(kubectl, version), quiet=True, echo_off=True)
39+
40+
print("Waiting for cert-manager to start")
41+
local("{} wait --for=condition=Available --timeout=300s -n cert-manager deployment/cert-manager".format(kubectl), quiet=True, echo_off=True)
42+
local("{} wait --for=condition=Available --timeout=300s -n cert-manager deployment/cert-manager-cainjector".format(kubectl), quiet=True, echo_off=True)
43+
local("{} wait --for=condition=Available --timeout=300s -n cert-manager deployment/cert-manager-webhook".format(kubectl), quiet=True, echo_off=True)
44+
45+
# deploy boot-operator
46+
def deploy_boot():
47+
version = settings.get("boot_version")
48+
image = settings.get("boot_image")
49+
new_args = settings.get("new_args").get("boot")
50+
boot_uri = "https://github.com/ironcore-dev/boot-operator//config/dev"
51+
cmd = "{} apply -k {}".format(kubectl, boot_uri)
52+
local(cmd, quiet=True)
53+
54+
replace_args_with_new_args("boot-operator-system", "boot-operator-controller-manager", new_args)
55+
patch_image("boot-operator-system", "boot-operator-controller-manager", image)
56+
57+
def patch_image(namespace, name, image):
58+
patch = [{
59+
"op": "replace",
60+
"path": "/spec/template/spec/containers/0/image",
61+
"value": image,
62+
}]
63+
local("{} patch deployment {} -n {} --type json -p='{}'".format(kubectl, name, namespace, str(encode_json(patch)).replace("\n", "")))
64+
65+
def replace_args_with_new_args(namespace, name, extra_args):
66+
patch = [{
67+
"op": "replace",
68+
"path": "/spec/template/spec/containers/0/args",
69+
"value": extra_args,
70+
}]
71+
local("{} patch deployment {} -n {} --type json -p='{}'".format(kubectl, name, namespace, str(encode_json(patch)).replace("\n", "")))
72+
73+
def waitforsystem():
74+
print("Waiting for metal-operator to start")
75+
local("{} wait --for=condition=ready --timeout=300s -n metal-operator-system pod --all".format(kubectl), quiet=False, echo_off=True)
76+
77+
##############################
78+
# Actual work happens here
79+
##############################
80+
81+
deploy_cert_manager()
82+
83+
docker_build('controller', '.', target = 'manager')
84+
85+
deploy_boot()
86+
87+
yaml = kustomize('./config/dev')
88+
89+
k8s_yaml(yaml)

api/v1alpha1/bmc_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const (
1212
BMCType = "bmc"
1313
ProtocolRedfish = "Redfish"
1414
ProtocolRedfishLocal = "RedfishLocal"
15+
ProtocolRedfishKube = "RedfishKube"
1516
)
1617

1718
// BMCSpec defines the desired state of BMC

bmc/redfish_kube.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package bmc
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"sigs.k8s.io/controller-runtime/pkg/client"
11+
12+
"github.com/stmcginnis/gofish/redfish"
13+
v1 "k8s.io/api/batch/v1"
14+
corev1 "k8s.io/api/core/v1"
15+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"k8s.io/utils/ptr"
17+
)
18+
19+
const (
20+
metalJobLabel = "kube.ironcore.dev/job"
21+
registryURL = "http://metal-registry.metal-operator-system.svc.cluster.local:30000/register"
22+
)
23+
24+
var _ BMC = (*RedfishKubeBMC)(nil)
25+
26+
type KubeClient struct {
27+
client client.Client
28+
namespace string
29+
}
30+
31+
// RedfishKubeBMC is an implementation of the BMC interface for Redfish.
32+
type RedfishKubeBMC struct {
33+
*RedfishBMC
34+
*KubeClient
35+
}
36+
37+
// NewRedfishKubeBMCClient creates a new RedfishKubeBMC with the given connection details.
38+
func NewRedfishKubeBMCClient(
39+
ctx context.Context,
40+
endpoint, username, password string,
41+
basicAuth bool,
42+
c client.Client,
43+
ns string,
44+
) (BMC, error) {
45+
bmc, err := NewRedfishBMCClient(ctx, endpoint, username, password, basicAuth)
46+
if err != nil {
47+
return nil, err
48+
}
49+
redfishKubeBMC := &RedfishKubeBMC{
50+
RedfishBMC: bmc,
51+
KubeClient: &KubeClient{
52+
client: c,
53+
namespace: ns,
54+
},
55+
}
56+
57+
return redfishKubeBMC, nil
58+
}
59+
60+
// SetPXEBootOnce sets the boot device for the next system boot using Redfish.
61+
func (r *RedfishKubeBMC) SetPXEBootOnce(systemUUID string) error {
62+
system, err := r.getSystemByUUID(systemUUID)
63+
if err != nil {
64+
return fmt.Errorf("failed to get systems: %w", err)
65+
}
66+
var setBoot redfish.Boot
67+
// TODO: cover setting BootSourceOverrideMode with BIOS settings profile
68+
if system.Boot.BootSourceOverrideMode != redfish.UEFIBootSourceOverrideMode {
69+
setBoot = pxeBootWithSettingUEFIBootMode
70+
} else {
71+
setBoot = pxeBootWithoutSettingUEFIBootMode
72+
}
73+
if err := system.SetBoot(setBoot); err != nil {
74+
return fmt.Errorf("failed to set the boot order: %w", err)
75+
}
76+
netData := `{"networkInterfaces":[{"name":"dummy0","ipAddress":"127.0.0.2","macAddress":"aa:bb:cc:dd:ee:ff"}]`
77+
curlCmd := fmt.Sprintf(
78+
`apk add curl && curl -H 'Content-Type: application/json' \
79+
-d '{"SystemUUID":"%s","data":%s}}' \
80+
-X POST %s`,
81+
systemUUID, netData, registryURL)
82+
cmd := []string{
83+
"/bin/sh",
84+
"-c",
85+
curlCmd,
86+
}
87+
if err := r.createJob(context.TODO(), r.KubeClient.client, cmd, r.KubeClient.namespace, systemUUID); err != nil {
88+
return fmt.Errorf("failed to create job for system %s: %w", systemUUID, err)
89+
}
90+
return nil
91+
}
92+
93+
func (r RedfishKubeBMC) createJob(
94+
ctx context.Context,
95+
c client.Client,
96+
cmd []string,
97+
namespace,
98+
systemUUID string,
99+
) error {
100+
// Check if a job with the same label already exists
101+
jobList := &v1.JobList{}
102+
listOpts := []client.ListOption{
103+
client.InNamespace(namespace),
104+
client.MatchingLabels{metalJobLabel: systemUUID},
105+
}
106+
if err := c.List(ctx, jobList, listOpts...); err != nil {
107+
return fmt.Errorf("failed to list jobs: %w", err)
108+
}
109+
if len(jobList.Items) > 0 {
110+
return nil // Job already exists, do not create a new one
111+
}
112+
113+
job := &v1.Job{
114+
ObjectMeta: metav1.ObjectMeta{
115+
GenerateName: fmt.Sprintf("register-%s-", systemUUID),
116+
Namespace: namespace,
117+
Labels: map[string]string{
118+
metalJobLabel: systemUUID,
119+
},
120+
},
121+
Spec: v1.JobSpec{
122+
Template: corev1.PodTemplateSpec{
123+
ObjectMeta: metav1.ObjectMeta{
124+
Labels: map[string]string{
125+
metalJobLabel: systemUUID,
126+
},
127+
},
128+
Spec: corev1.PodSpec{
129+
Containers: []corev1.Container{
130+
{
131+
Name: "registry-job",
132+
Image: "alpine:latest",
133+
Command: cmd,
134+
},
135+
},
136+
RestartPolicy: corev1.RestartPolicyNever,
137+
},
138+
},
139+
TTLSecondsAfterFinished: ptr.To(int32(30)),
140+
},
141+
}
142+
if err := c.Create(ctx, job); err != nil {
143+
return err
144+
}
145+
return nil
146+
}

config/dev/kustomization.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
resources:
22
- ../default
3+
- ../redfish-mockup
4+
- registry_service.yaml
35

46
patches:
57
- path: delete_manager_auth_proxy_patch.yaml
8+
- path: manager_patch.yaml
9+
10+
secretGenerator:
11+
- name: macdb
12+
namespace: metal-operator-system
13+
files:
14+
- macdb.yaml

config/dev/macdb.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
macPrefixes:
2+
- macPrefix: 23
3+
manufacturer: Foo
4+
protocol: RedfishKube
5+
port: 8000
6+
type: bmc
7+
defaultCredentials:
8+
- username: foo
9+
password: bar
10+
console:
11+
type: ssh
12+
port: 22

config/dev/manager_patch.yaml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: controller-manager
5+
namespace: system
6+
spec:
7+
template:
8+
spec:
9+
containers:
10+
- name: manager
11+
args:
12+
- --health-probe-bind-address=:8081
13+
- --metrics-bind-address=127.0.0.1:8080
14+
- --leader-elect
15+
- --mac-prefixes-file=/etc/macdb/macdb.yaml
16+
- --probe-image=ghcr.io/ironcore-dev/metalprobe:latest
17+
- --probe-os-image=ghcr.io/ironcore-dev/os-images/gardenlinux:1443.10
18+
- --registry-url=http://127.0.0.1:30000
19+
- --registry-port=30000
20+
- --enforce-first-boot
21+
ports:
22+
- containerPort: 30000
23+
volumeMounts:
24+
- mountPath: /etc/macdb/
25+
name: macdb
26+
- name: redfish
27+
image: dmtf/redfish-mockup-server:latest
28+
ports:
29+
- containerPort: 8000
30+
securityContext:
31+
runAsNonRoot: false
32+
volumes:
33+
- name: macdb
34+
secret:
35+
defaultMode: 420
36+
secretName: macdb

config/dev/registry_service.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
name: metal-registry
5+
namespace: metal-operator-system
6+
spec:
7+
ports:
8+
- name: registry-server
9+
port: 30000
10+
targetPort: 30000
11+
selector:
12+
control-plane: controller-manager

0 commit comments

Comments
 (0)