Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
eebc3bf
Draft initial implementation
simu Jun 18, 2026
c070890
Make privnet name a valid K8s name
simu Jun 18, 2026
caad8f9
Set `spec.version=params.kubernetesVersion` in TalosControlPlane cust…
simu Jun 18, 2026
14c6104
add comment re cloudscale creds secret name
simu Jun 18, 2026
d793f40
Fix controlplane patch merge
simu Jun 18, 2026
5a15dbf
Set MachineDeployment `spec.version=kubernetesVersion`
simu Jun 18, 2026
8d69bc6
Deploy cilium via ClusterResourceSet
simu Jun 23, 2026
430dda5
Refactor component to generate unique names for manifests with immuta…
simu Jun 23, 2026
80b3cde
Adjust Talos machineconfig to deploy Cilium via `ClusterResourceSet` …
simu Jun 23, 2026
5c2dc5e
Update TalosControlPlane api version to v1beta1
simu Jun 23, 2026
ac3dd81
Propagate `controlPlane.count` to `TalosControlPlane`
simu Jun 25, 2026
7a8d86c
Add parameter to set API URL as extra Talos API server cert SAN
simu Jun 26, 2026
45ae76c
Add talosconfig patch todo
simu Jun 26, 2026
45d2c3c
CAPI provider canonical Kubernetes version format is prefixed with `v`
simu Jul 2, 2026
fb1596b
Set Talos `MachineConfig` `install.wipe=true` by default
simu Jul 2, 2026
6b39fe8
Give users more control over TalosControlPlane config
simu Jul 3, 2026
3d63edf
Add parameter `strategicPatches` to apply custom patches to all nodes
simu Jul 3, 2026
9fb9e29
Set `machine.install.image` on all nodes
simu Jul 3, 2026
6f03adf
Configure ArgoCD to ignore changes to `spec.replicas` of MachineDeplo…
simu Jul 3, 2026
58f491b
Make MachineDeployment deletion order configurable
simu Jul 3, 2026
588e0f0
Update component to make parameter `talosVersion` less misleading
simu Jul 6, 2026
f301c81
Add logic to render and configure a K8s API server `AuthenticationCon…
simu Aug 3, 2026
8a44d78
Remove machinedeployment `spec.selector`
simu Aug 12, 2026
49f9e9a
Enable server-side apply in ArgoCD app
simu Aug 12, 2026
8c94468
Enable kubelet server certificate rotation
simu Aug 13, 2026
0a88e69
Draft: user kubeconfig server & cluster ca dynamic fact
simu Aug 12, 2026
17cb4eb
Add simple "copy" button to kubeconfig shown in page
simu Aug 12, 2026
4a86e4e
Restyle "download" link to look the same as the "copy" button
simu Aug 12, 2026
84da571
Fix selection when clicking into code block
simu Aug 12, 2026
8a03ff6
Update golden test for download page
simu Aug 13, 2026
033e1e2
Initial "first time setup" section on user kubeconfig index page
simu Aug 13, 2026
62a0a41
Add cluster CA certificate as separate file in user kubeconfig server
simu Aug 14, 2026
84dd5f7
Adjust numbered list alignment on user kubeconfig index page
simu Aug 14, 2026
af505dd
Add NET_BIND_SERVICE capability for caddy container
simu Aug 26, 2026
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
80 changes: 79 additions & 1 deletion class/defaults.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,83 @@
parameters:
talos_capi_cluster_cloudscale:
=_metadata:
multi_instance: true
multi_tenant: true
namespace: syn-talos-capi-cluster-cloudscale
namespace: syn-cluster-api

clusterName: ${cluster:name}

# TODO(sg): how will updates work with CAPI?
# IMPORTANT: this is only used for ensuring CAPI renders a suitable
# MachineConfig and isn't used to define the cluster's actual Talos
# version (at least when using the OpenStack raw image from the image
# factory).
talosVersion: '1.13'
# NOTE(sg): this is the well-known default schematic UUID
talosSchematicUUID: 376567988ad370138ad8b2698212367b8edcb69b5fd68c80be1f2ec7d603b4ba
kubernetesVersion: 'v1.36.1'
apiURL: ""

cni: cilium

cloudscale:
# TODO(sg): document this
customImageSlug: talos-v${talos_capi_cluster_cloudscale:talosVersion}-37656798
# TODO(sg): decide on default sizing
privateNetwork:
# TODO(sg): does this need to match the privnet name on cloudscale?
name: privnet-${talos_capi_cluster_cloudscale:clusterName}
uuid: TO_BE_REPLACED
region: ${facts:region}

controlPlane:
count: 1
flavor: plus-16-4
rootVolumeSize: 50

workerGroups:
worker:
count: 1
flavor: plus-16-4
rootVolumeSize: 50

cluster:
spec:
clusterNetwork:
pods:
cidrBlocks:
- 10.128.0.0/14
services:
cidrBlocks:
- 172.30.0.0/16
serviceDomain: cluster.local

cloudscaleCluster:
spec:
region: ${talos_capi_cluster_cloudscale:cloudscale:region}
credentialsRef:
# TODO(sg): figure out the real default name here.
name: cloudscale-credentials

talosControlPlane:
metadata: {}
spec: {}
strategicPatches: {}

talosStrategicPatches: {}

kubernetesApiServer:
authenticationConfigurationJWT: {}

## Temporary config for user kubeconfig service
images:
caddy:
registry: docker.io
repository: caddy/caddy
tag: 2.11.4-alpine
kubeconfig:
serverURL: ""
ingressAnnotations: {}
oidc:
issuerURL: ""
clientId: ""
1 change: 1 addition & 0 deletions class/talos-capi-cluster-cloudscale.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ parameters:
output_path: .
- input_paths:
- ${_base_directory}/component/main.jsonnet
- ${_base_directory}/component/user-kubeconfig.jsonnet
input_type: jsonnet
output_path: talos-capi-cluster-cloudscale/
18 changes: 17 additions & 1 deletion component/app.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,23 @@ local inv = kap.inventory();
local params = inv.parameters.talos_capi_cluster_cloudscale;
local argocd = import 'lib/argocd.libjsonnet';

local app = argocd.App('talos-capi-cluster-cloudscale', params.namespace);
local app = argocd.App('talos-capi-cluster-cloudscale', params.namespace) {
spec+: {
ignoreDifferences+: [
{
group: 'cluster.x-k8s.io',
kind: 'MachineDeployment',
jsonPointers: [ '/spec/replicas' ],
},
],
syncPolicy+: {
syncOptions+: [
'RespectIgnoreDifferences=true',
'ServerSideApply=true',
],
},
},
};

local appPath =
local project = std.get(std.get(app, 'spec', {}), 'project', 'syn');
Expand Down
116 changes: 116 additions & 0 deletions component/assets/user-kubeconfig-index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ env "CLUSTER_NAME" }} Kubeconfig</title>
<style>
body {
font-family: 'sans-serif';
width: 50%;
margin: 0 auto;
}

pre {
font-size: 120%;
display: block;
background: #ddd;
border: 1px solid #333;
white-space: pre-wrap;
word-wrap: break-word;
padding: 1ex;
}
code {
user-select: all;
}
ol {
padding: 0 1.5em;
}
li > code {
font-size: 120%;
}

* :has(> [class="copy"]) {
position: relative;
}

[class="copy"]::after {
content: "[copy]";
position: absolute;
right: 6.5em;
top: 0;
margin: 0.5em;
}
li [class="copy"]::after {
right: 0;
}
li {
margin: 0.7ex 0;
}
li > pre {
margin-top: 0.5ex;
margin-bottom: 0.5ex;
}

a.download {
position: absolute;
right: 0;
top: 0;
margin: 0.5em;
color: black;
text-decoration: none;
}

a.download:hover {
font-weight: bold;
}

[class="copy"]:hover::after {
font-weight: bold;
cursor: pointer;
}

.copied > [class="copy"]::after {
content: "[copied]";
}
</style>
<script>
window.onload = () => {
document.querySelectorAll('[class="copy"]').forEach($el =>
$el.addEventListener('click', async ($el) => {
const $node = $el.target;
const content = $node.textContent;
try {
await navigator.clipboard.writeText(content);
} catch (error) {
console.error(error.message);
}
$node.parentNode.classList.add('copied');
setTimeout(($n => () => $n.classList.remove('copied'))($node.parentNode), 1000);
})
);
}
</script>
</head>
<body>
<h1>{{ env "CLUSTER_NAME" }} Kubeconfig</h1>
<p>
<!-- NOTE(sg): the whole <pre> contents must be in a single line to not mess up the formatting! -->
<pre><a class="download" href="./kubeconfig" download="{{ env "CLUSTER_NAME" }}.kubeconfig">[download]</a><code class="copy">{{- readFile "kubeconfig" -}}</code></pre>
</p>
<h1>First time setup</h1>
<ol>
<li>Make sure you have a recent <code>kubectl</code> installed. See the <a href="https://kubernetes.io/docs/tasks/tools/#kubectl">upstream docs</a> for install instructions.</li>
<li>Download the <code>kubectl</code> <a href="https://github.com/int128/kubelogin#setup" target="_blank">kubelogin</a> plugin and ensure it&rsquo;s available in your <code>$PATH</code> as <code>kubectl-oidc_login</code></li>
<li>Verify that the plugin is available.<br/>
<pre class="li"><code class="copy">kubectl oidc-login --version</code></pre>
</li>
<li>Download this cluster&rsquo;s <a href="./kubeconfig" download="{{ env "CLUSTER_NAME" }}.kubeconfig">kubeconfig</a></li>
<li>Set the <code>$KUBECONFIG</code> environment variable<br/>
<pre class="li"><code class="copy">export KUBECONFIG=~/Downloads/{{ env "CLUSTER_NAME" }}.kubeconfig</code></pre>
</li>
<li>Test access<br/>
<pre class="li"><code class="copy">kubectl auth whoami</code></pre>
If everything is setup correctly, this should authenticate you with your VSHN account in your browser and then show some details about your user.
</li>
</ol>
</body>
</html>
121 changes: 121 additions & 0 deletions component/espejote-templates/kubeconfig-ca-manager.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
local esp = import 'espejote.libsonnet';

local config = import 'capi-kubeconfig-ca-manager/config.json';

local kubeconfig =
local kcs = esp.context().kubeconfig;
assert std.length(kcs) == 1 : 'Expected kubeconfig context to have length 1';
assert std.objectHas(kcs[0].data, 'value') : 'Expected kubeconfig secret to have field `value`';
std.parseYaml(std.base64Decode(kcs[0].data.value));

local cluster_ca = std.base64Decode(
kubeconfig.clusters[0].cluster['certificate-authority-data']
);

local user_kubeconfig =
local contextName = 'oidc@%s' % kubeconfig.clusters[0].name;
{
apiVersion: 'v1',
kind: 'Config',
clusters: [
kubeconfig.clusters[0] {
cluster+: {
[if config.apiURL != '' then 'server']:
'https://%s:6443' % config.apiURL,
},
},
],
contexts: [
{
context: {
cluster: kubeconfig.clusters[0].name,
user: 'oidc',
},
name: contextName,
},
],
'current-context': contextName,
users: [
{
name: 'oidc',
user: {
exec: {
apiVersion: 'client.authentication.k8s.io/v1beta1',
args: [
'oidc-login',
'get-token',
'--oidc-issuer-url=%s' % config.oidcIssuerURL,
'--oidc-client-id=%s' % config.oidcClientId,
'--oidc-extra-scope=email offline_access profile openid',
],
command: 'kubectl',
interactiveMode: 'IfAvailable',
provideClusterInfo: false,
},
},
},
],
};
local index_html = importstr 'capi-kubeconfig-ca-manager/index.html';
local caddy_json = importstr 'capi-kubeconfig-ca-manager/caddy.json';
local confighash = std.sha256(
std.manifestJsonMinified(user_kubeconfig) + index_html + caddy_json
);

[
{
apiVersion: 'v1',
kind: 'ConfigMap',
metadata: {
name: config.dynfactConfigMapName,
namespace: 'syn',
labels: {
'app.kubernetes.io/managed-by': 'espejote',
'steward.syn.tools/include-facts': '',
},
},
data: {
facts: std.manifestJsonMinified({
talosAPICertificateAuthorityData:
kubeconfig.clusters[0].cluster['certificate-authority-data'],
}),
},
},
{
apiVersion: 'v1',
kind: 'ConfigMap',
metadata: {
name: config.caddyResourceName,
namespace: config.namespace,
},
data: {
// manifestYamlDoc doesn't add a trailing newline, so we do it
// ourselves.
kubeconfig: std.manifestYamlDoc(user_kubeconfig, quote_keys=false) + '\n',
'cluster-ca.crt': cluster_ca,
'index.html': index_html,
'caddy.json': caddy_json,
},
},
esp.applyOptions(
{
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: {
name: config.caddyResourceName,
namespace: config.namespace,
},
spec: {
template: {
metadata: {
annotations: {
['%s.syn.tools/config-hash' % config.caddyResourceName]: confighash,
},
},
},
},
},
fieldManagerSuffix=':reloader',
force=true,
),
]
Loading
Loading