Skip to content

Commit

Permalink
support airgap install
Browse files Browse the repository at this point in the history
Signed-off-by: nasusoba <[email protected]>
  • Loading branch information
nasusoba committed Apr 17, 2024
1 parent 09c603f commit bfec5cd
Show file tree
Hide file tree
Showing 16 changed files with 232 additions and 20 deletions.
7 changes: 7 additions & 0 deletions bootstrap/api/v1beta1/kthreesconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ type KThreesAgentConfig struct {
// NodeName Name of the Node
// +optional
NodeName string `json:"nodeName,omitempty"`

// AirGapped is a boolean value to define if the bootstrapping should be air-gapped,
// basically supposing that online container registries and k3s install scripts are not reachable.
// User should prepare docker image, k3s binary, and put the install script in `/opt/install.sh`
// on all nodes in the air-gap environment.
// +optional
AirGapped bool `json:"airGapped,omitempty"`
}

// KThreesConfigStatus defines the observed state of KThreesConfig.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ spec:
agentConfig:
description: AgentConfig specifies configuration for the agent nodes
properties:
airGapped:
description: |-
AirGapped is a boolean value to define if the bootstrapping should be air-gapped,
basically supposing that online container registries and k3s install scripts are not reachable.
User should prepare docker image, k3s binary, and put the install script in `/opt/install.sh`
on all nodes in the air-gap environment.
type: boolean
kubeProxyArgs:
description: KubeProxyArgs Customized flag for kube-proxy process
items:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ spec:
description: AgentConfig specifies configuration for the agent
nodes
properties:
airGapped:
description: |-
AirGapped is a boolean value to define if the bootstrapping should be air-gapped,
basically supposing that online container registries and k3s install scripts are not reachable.
User should prepare docker image, k3s binary, and put the install script in `/opt/install.sh`
on all nodes in the air-gap environment.
type: boolean
kubeProxyArgs:
description: KubeProxyArgs Customized flag for kube-proxy
process
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ spec:
description: AgentConfig specifies configuration for the agent
nodes
properties:
airGapped:
description: |-
AirGapped is a boolean value to define if the bootstrapping should be air-gapped,
basically supposing that online container registries and k3s install scripts are not reachable.
User should prepare docker image, k3s binary, and put the install script in `/opt/install.sh`
on all nodes in the air-gap environment.
type: boolean
kubeProxyArgs:
description: KubeProxyArgs Customized flag for kube-proxy
process
Expand Down
3 changes: 3 additions & 0 deletions bootstrap/controllers/kthreesconfig_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ func (r *KThreesConfigReconciler) joinControlplane(ctx context.Context, scope *S
AdditionalFiles: files,
ConfigFile: workerConfigFile,
K3sVersion: scope.Config.Spec.Version,
AirGapped: scope.Config.Spec.AgentConfig.AirGapped,
},
}

Expand Down Expand Up @@ -328,6 +329,7 @@ func (r *KThreesConfigReconciler) joinWorker(ctx context.Context, scope *Scope)
AdditionalFiles: files,
ConfigFile: workerConfigFile,
K3sVersion: scope.Config.Spec.Version,
AirGapped: scope.Config.Spec.AgentConfig.AirGapped,
},
}

Expand Down Expand Up @@ -483,6 +485,7 @@ func (r *KThreesConfigReconciler) handleClusterNotInitialized(ctx context.Contex
AdditionalFiles: files,
ConfigFile: initConfigFile,
K3sVersion: scope.Config.Spec.Version,
AirGapped: scope.Config.Spec.AgentConfig.AirGapped,
},
Certificates: certificates,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ spec:
agentConfig:
description: AgentConfig specifies configuration for the agent nodes
properties:
airGapped:
description: |-
AirGapped is a boolean value to define if the bootstrapping should be air-gapped,
basically supposing that online container registries and k3s install scripts are not reachable.
User should prepare docker image, k3s binary, and put the install script in `/opt/install.sh`
on all nodes in the air-gap environment.
type: boolean
kubeProxyArgs:
description: KubeProxyArgs Customized flag for kube-proxy process
items:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ spec:
description: AgentConfig specifies configuration for the agent
nodes
properties:
airGapped:
description: |-
AirGapped is a boolean value to define if the bootstrapping should be air-gapped,
basically supposing that online container registries and k3s install scripts are not reachable.
User should prepare docker image, k3s binary, and put the install script in `/opt/install.sh`
on all nodes in the air-gap environment.
type: boolean
kubeProxyArgs:
description: KubeProxyArgs Customized flag for kube-proxy
process
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ spec:
description: AgentConfig specifies configuration for the agent
nodes
properties:
airGapped:
description: |-
AirGapped is a boolean value to define if the bootstrapping should be air-gapped,
basically supposing that online container registries and k3s install scripts are not reachable.
User should prepare docker image, k3s binary, and put the install script in `/opt/install.sh`
on all nodes in the air-gap environment.
type: boolean
kubeProxyArgs:
description: KubeProxyArgs Customized flag for kube-proxy
process
Expand Down
17 changes: 10 additions & 7 deletions pkg/cloudinit/cloudinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,20 @@ write_files:{{ range . }}
{{- end -}}
{{- end -}}
`
sentinelFileCommand = "mkdir -p /run/cluster-api && echo success > /run/cluster-api/bootstrap-success.complete"
)

// BaseUserData is shared across all the various types of files written to disk.
type BaseUserData struct {
Header string
PreK3sCommands []string
PostK3sCommands []string
AdditionalFiles []bootstrapv1.File
WriteFiles []bootstrapv1.File
ConfigFile bootstrapv1.File
K3sVersion string
Header string
PreK3sCommands []string
PostK3sCommands []string
AdditionalFiles []bootstrapv1.File
WriteFiles []bootstrapv1.File
ConfigFile bootstrapv1.File
K3sVersion string
AirGapped bool
SentinelFileCommand string
}

func generate(kind string, tpl string, data interface{}) ([]byte, error) {
Expand Down
3 changes: 2 additions & 1 deletion pkg/cloudinit/controlplane_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const (
{{template "files" .WriteFiles}}
runcmd:
{{- template "commands" .PreK3sCommands }}
- 'curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=%s sh -s - server && mkdir -p /run/cluster-api && echo success > /run/cluster-api/bootstrap-success.complete'
- {{ if .AirGapped }} INSTALL_K3S_SKIP_DOWNLOAD=true INSTALL_K3S_EXEC='server' sh /opt/install.sh {{ else }} curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=%s sh -s - server {{ end }} && {{ .SentinelFileCommand }}
{{- template "commands" .PostK3sCommands }}
`
)
Expand All @@ -44,6 +44,7 @@ func NewInitControlPlane(input *ControlPlaneInput) ([]byte, error) {
input.WriteFiles = input.Certificates.AsFiles()
input.WriteFiles = append(input.WriteFiles, input.AdditionalFiles...)
input.WriteFiles = append(input.WriteFiles, input.ConfigFile)
input.SentinelFileCommand = sentinelFileCommand

controlPlaneCloudJoinWithVersion := fmt.Sprintf(controlPlaneCloudInit, input.K3sVersion)
userData, err := generate("InitControlplane", controlPlaneCloudJoinWithVersion, input)
Expand Down
30 changes: 30 additions & 0 deletions pkg/cloudinit/controlplane_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,33 @@ func TestControlPlaneInit(t *testing.T) {
g.Expect(err).NotTo(HaveOccurred())
t.Log(string(out))
}

func TestControlPlaneInitAirGapped(t *testing.T) {
g := NewWithT(t)

cpinput := &ControlPlaneInput{
BaseUserData: BaseUserData{
PreK3sCommands: nil,
PostK3sCommands: nil,
AdditionalFiles: []infrav1.File{
{
Path: "/tmp/my-path",
Encoding: infrav1.Base64,
Content: "aGk=",
},
{
Path: "/tmp/my-other-path",
Content: "hi",
},
},
AirGapped: true,
},
Certificates: secret.Certificates{},
}

out, err := NewInitControlPlane(cpinput)
g.Expect(err).NotTo(HaveOccurred())
result := string(out)
g.Expect(result).To(ContainSubstring("sh /opt/install.sh"))
g.Expect(result).NotTo(ContainSubstring("get.k3s.io"))
}
14 changes: 3 additions & 11 deletions pkg/cloudinit/controlplane_join.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,15 @@ package cloudinit

import "fmt"

const (
controlPlaneCloudJoin = `{{.Header}}
{{template "files" .WriteFiles}}
runcmd:
{{- template "commands" .PreK3sCommands }}
- 'curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=%s sh -s - server && mkdir -p /run/cluster-api && echo success > /run/cluster-api/bootstrap-success.complete'
{{- template "commands" .PostK3sCommands }}
`
)

// NewInitControlPlane returns the user data string to be used on a controlplane instance.
func NewJoinControlPlane(input *ControlPlaneInput) ([]byte, error) {
input.Header = cloudConfigHeader
input.WriteFiles = append(input.WriteFiles, input.AdditionalFiles...)
input.WriteFiles = append(input.WriteFiles, input.ConfigFile)
input.SentinelFileCommand = sentinelFileCommand

controlPlaneCloudJoinWithVersion := fmt.Sprintf(controlPlaneCloudJoin, input.K3sVersion)
// As controlPlaneCloudJoin template is the same as the controlPlaneCloudInit template, will reuse the controlPlaneCloudInit template
controlPlaneCloudJoinWithVersion := fmt.Sprintf(controlPlaneCloudInit, input.K3sVersion)
userData, err := generate("JoinControlplane", controlPlaneCloudJoinWithVersion, input)
if err != nil {
return nil, err
Expand Down
3 changes: 2 additions & 1 deletion pkg/cloudinit/worker_join.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const (
{{template "files" .WriteFiles}}
runcmd:
{{- template "commands" .PreK3sCommands }}
- 'curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=%s sh -s - agent && mkdir -p /run/cluster-api && echo success > /run/cluster-api/bootstrap-success.complete'
- {{ if .AirGapped }} INSTALL_K3S_SKIP_DOWNLOAD=true INSTALL_K3S_EXEC='agent' sh /opt/install.sh {{ else }} curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=%s sh -s - agent {{ end }} && {{ .SentinelFileCommand }}
{{- template "commands" .PostK3sCommands }}
`
)
Expand All @@ -38,6 +38,7 @@ func NewWorker(input *WorkerInput) ([]byte, error) {
input.Header = cloudConfigHeader
input.WriteFiles = append(input.WriteFiles, input.AdditionalFiles...)
input.WriteFiles = append(input.WriteFiles, input.ConfigFile)
input.SentinelFileCommand = sentinelFileCommand

workerCloudInitWithVersion := fmt.Sprintf(workerCloudInit, input.K3sVersion)
userData, err := generate("Worker", workerCloudInitWithVersion, input)
Expand Down
15 changes: 15 additions & 0 deletions samples/docker/air-gapped/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM kindest/node:v1.28.0
ARG K3S_VERSION=v1.28.6+k3s2

# Load docker images
# Note that the flow follows the manually deploy image steps, but private registry method should also be supported
RUN mkdir -p /var/lib/rancher/k3s/agent/images/
RUN curl -L -o /var/lib/rancher/k3s/agent/images/k3s-airgap-images-amd64.tar.zst "https://github.com/k3s-io/k3s/releases/download/${K3S_VERSION}/k3s-airgap-images-amd64.tar.zst"

# Download install script to /opt/install.sh
RUN curl -L -o /opt/install.sh https://get.k3s.io
RUN chmod +x /opt/install.sh

# Download k3s binary
RUN curl -L -o /usr/local/bin/k3s "https://github.com/k3s-io/k3s/releases/download/${K3S_VERSION}/k3s"
RUN chmod +x /usr/local/bin/k3s
23 changes: 23 additions & 0 deletions samples/docker/air-gapped/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Air-gapped K3s Cluster

K3s is supporting air-gapped installations. This sample demonstrates how to create a K3s cluster in an air-gapped environment with cluster API k3s and Docker.

It will first build a kind node docker image with the K3s binary, the required images and scripts, following [k3s Air-Gap Install](https://docs.k3s.io/installation/airgap). Then it will create a K3s cluster with this kind node image.

```shell
export AIRGAPPED_KIND_IMAGE=kindnode:airgapped
export CLUSTER_NAME=k3s-airgapped
export CONTROL_PLANE_MACHINE_COUNT=1
export KUBERNETES_VERSION=v1.28.6+k3s2
export WORKER_MACHINE_COUNT=3

# Build the kind node image
docker build -t $AIRGAPPED_KIND_IMAGE . --build-arg="K3S_VERSION=$KUBERNETES_VERSION"

# Generate the cluster yaml
# Note that `airGapped` is set to true in `agentConfig`
clusterctl generate yaml --from ./k3s-template.yaml > k3s-airgapped.yaml

# Create the cluster to the management cluster
kubectl apply -f k3s-airgapped.yaml
```
95 changes: 95 additions & 0 deletions samples/docker/air-gapped/k3s-template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
apiVersion: cluster.x-k8s.io/v1beta1
kind: Cluster
metadata:
name: ${CLUSTER_NAME}
spec:
clusterNetwork:
pods:
cidrBlocks:
- 10.45.0.0/16
services:
cidrBlocks:
- 10.46.0.0/16
serviceDomain: cluster.local
controlPlaneRef:
apiVersion: controlplane.cluster.x-k8s.io/v1beta1
kind: KThreesControlPlane
name: ${CLUSTER_NAME}-control-plane
infrastructureRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: DockerCluster
name: ${CLUSTER_NAME}
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: DockerCluster
metadata:
name: ${CLUSTER_NAME}
spec: {}
---
apiVersion: controlplane.cluster.x-k8s.io/v1beta1
kind: KThreesControlPlane
metadata:
name: ${CLUSTER_NAME}-control-plane
namespace: default
spec:
infrastructureTemplate:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: DockerMachineTemplate
name: ${CLUSTER_NAME}-control-plane
kthreesConfigSpec:
agentConfig:
airGapped: true
replicas: ${CONTROL_PLANE_MACHINE_COUNT}
version: ${KUBERNETES_VERSION}
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: DockerMachineTemplate
metadata:
name: ${CLUSTER_NAME}-control-plane
spec:
template:
spec:
customImage: ${AIRGAPPED_KIND_IMAGE}
---
apiVersion: cluster.x-k8s.io/v1beta1
kind: MachineDeployment
metadata:
name: worker-md-0
spec:
clusterName: ${CLUSTER_NAME}
replicas: ${WORKER_MACHINE_COUNT}
selector:
matchLabels:
cluster.x-k8s.io/cluster-name: ${CLUSTER_NAME}
template:
spec:
version: ${KUBERNETES_VERSION}
clusterName: ${CLUSTER_NAME}
bootstrap:
configRef:
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: KThreesConfigTemplate
name: ${CLUSTER_NAME}-md-0
infrastructureRef:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: DockerMachineTemplate
name: ${CLUSTER_NAME}-md-0
---
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: DockerMachineTemplate
metadata:
name: ${CLUSTER_NAME}-md-0
spec:
template:
spec:
customImage: ${AIRGAPPED_KIND_IMAGE}
---
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: KThreesConfigTemplate
metadata:
name: ${CLUSTER_NAME}-md-0
spec:
template:
spec:
agentConfig:
airGapped: true

0 comments on commit bfec5cd

Please sign in to comment.