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
9 changes: 9 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ layout:
projectName: kodiak
repo: github.com/mNi-Cloud/kodiak
resources:
- api:
crdVersion: v1
namespaced: true
controller: true
domain: mnicloud.jp
group: kodiak
kind: ConnectorInstance
path: github.com/mNi-Cloud/kodiak/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
Expand Down
29 changes: 18 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Kodiak

Kodiak is a Kubernetes operator for stable Tailscale subnet-router workloads.
Kodiak is a Kubernetes operator for logical Tailscale subnet-router services.
It can use either:

- an existing Tailscale-compatible control plane and auth-key Secret; or
Expand All @@ -11,16 +11,18 @@ Kodiak does not install Ionscale, a DERP server, or cert-manager.

## API

- `Connector` creates a kernel-networking `StatefulSet`. Every replica has a
stable Pod name and a dedicated Kubernetes Secret containing containerboot
state. Kodiak advertises routes but never approves them.
- `Connector` defines a logical subnet-router service. Every replica slot is
fulfilled by a replaceable `ConnectorInstance` and a direct kernel-networking
Pod. Kodiak advertises routes but never approves them.
- `ConnectorInstance` is an internal lifecycle record for one Pod incarnation
and its external device. It does not contain tailscaled private state.
- `Tailnet` manages Ionscale Tailnet policy, DNS, and feature settings.
- `AuthKey` issues user/manual enrollment credentials for a managed Tailnet.
Connectors do not depend on `AuthKey` resources.

Managed Connectors use a `tailnetRef`. Kodiak creates a short-lived,
pre-authorized bootstrap credential for each replica, waits for containerboot
to persist the device identity, then revokes the bootstrap credential. Route
Managed Connectors use a `tailnetRef`. Kodiak creates a short-lived, ephemeral,
pre-authorized bootstrap credential for each new instance, records the
registered device, then revokes and deletes the bootstrap credential. Route
approval belongs in the Tailnet ACL `autoApprovers` policy.

External Connectors use an `authKeySecretRef`. If `loginURL` is omitted, the
Expand Down Expand Up @@ -51,7 +53,7 @@ For Kustomize development installs:

```sh
make install
make deploy IMG=ghcr.io/mni-cloud/kodiak:0.2.0
make deploy IMG=ghcr.io/mni-cloud/kodiak:0.2.1
```

Patch `IONSCALE_API_ENDPOINT`, `IONSCALE_LOGIN_URL`, and the optional
Expand Down Expand Up @@ -107,9 +109,14 @@ Kodiak exposes advertised and enabled routes separately in
## Security boundary

Kodiak never calls the Ionscale route-enable API; `autoApprovers` is the only
managed route-approval authority. Connector bootstrap keys stay in
controller-owned replica State Secrets and are revoked after identity is
persisted.
managed route-approval authority. Managed bootstrap keys are transient
controller-owned Secrets and are revoked after the external device is
recorded.

Connector Pods use the stock Tailscale image with an `emptyDir` state
directory. They do not receive a service-account token or Kubernetes API
credentials. A Pod replacement intentionally creates a new external device;
Kodiak brings it to readiness before deleting the previous instance.

Ionscale must also enforce that tags requested during registration are a
subset of the tags carried by the auth key. Until that control-plane check is
Expand Down
19 changes: 11 additions & 8 deletions api/v1alpha1/connector_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ import (

const (
ConnectorConditionReady = "Ready"
ConnectorConditionAvailable = "Available"
ConnectorConditionWorkloadReady = "WorkloadReady"
ConnectorConditionIdentityReady = "IdentityReady"
ConnectorConditionControlPlaneReady = "ControlPlaneReady"
ConnectorConditionRoutesReady = "RoutesReady"
)

// ConnectorSpec describes a kernel-networking Tailscale subnet router.
// ConnectorSpec describes a logical kernel-networking Tailscale subnet-router
// service. Individual Tailscale devices are replaceable implementation
// resources and are not stable identities.
//
// A managed Connector references a Kodiak Tailnet. Kodiak creates short-lived
// bootstrap credentials for each replica and observes the corresponding
Expand Down Expand Up @@ -78,7 +80,7 @@ type ConnectorSpec struct {
// +required
SubnetRouter SubnetRouterSpec `json:"subnetRouter"`

// Workload customizes the generated StatefulSet without exposing
// Workload customizes the generated Pods without exposing
// Tailscale implementation flags.
// +optional
Workload ConnectorWorkloadSpec `json:"workload,omitempty"`
Expand Down Expand Up @@ -130,7 +132,7 @@ type ConnectorPodMetadata struct {
Annotations map[string]string `json:"annotations,omitempty"`
}

// ConnectorStatus reports workload, identity, and managed control-plane state.
// ConnectorStatus reports aggregate workload and managed control-plane state.
type ConnectorStatus struct {
// ObservedGeneration is the most recent generation reconciled.
// +optional
Expand All @@ -140,7 +142,8 @@ type ConnectorStatus struct {
// +optional
ManagedTailnetID string `json:"managedTailnetID,omitempty"`

// Devices reports one stable identity per desired replica.
// Devices reports the active external device for each desired replica slot.
// Device IDs, addresses, and hostnames can change when a Pod is replaced.
// +optional
// +listType=map
// +listMapKey=ordinal
Expand All @@ -153,12 +156,12 @@ type ConnectorStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// ConnectorDeviceStatus is the observed state for one StatefulSet ordinal.
// ConnectorDeviceStatus is the observed state for one replaceable replica.
type ConnectorDeviceStatus struct {
// Ordinal identifies the StatefulSet replica.
// Ordinal identifies the logical replica slot.
Ordinal int32 `json:"ordinal"`

// DeviceID is the stable Tailscale node ID written by containerboot.
// DeviceID is the provider's current Tailscale node ID when available.
// +optional
DeviceID string `json:"deviceID,omitempty"`

Expand Down
147 changes: 147 additions & 0 deletions api/v1alpha1/connectorinstance_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
Copyright 2025.

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 (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const ConnectorInstanceConditionReady = "Ready"

// ConnectorInstanceSpec is an immutable snapshot of one Connector replica
// incarnation. ConnectorInstance is an implementation resource owned by a
// Connector, not a user-facing identity resource.
//
// +kubebuilder:validation:XValidation:rule="has(self.managedTailnetID) != has(self.authKeySecretRef)",message="exactly one of managedTailnetID or authKeySecretRef must be specified"
type ConnectorInstanceSpec struct {
// ConnectorRef identifies the owning logical Connector.
// +required
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="connectorRef is immutable"
ConnectorRef corev1.LocalObjectReference `json:"connectorRef"`

// Slot is the logical replica position served by this incarnation.
// +kubebuilder:validation:Minimum=0
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="slot is immutable"
Slot int32 `json:"slot"`

// Revision changes whenever the resolved Connector configuration changes.
// +required
// +kubebuilder:validation:MinLength=8
// +kubebuilder:validation:MaxLength=64
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="revision is immutable"
Revision string `json:"revision"`

// ManagedTailnetID selects a Kodiak-managed Ionscale Tailnet. Empty means
// the control plane is externally managed.
// +optional
// +kubebuilder:validation:Pattern=`^[0-9]+$`
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="managedTailnetID is immutable"
ManagedTailnetID string `json:"managedTailnetID,omitempty"`

// LoginURL is the Tailscale protocol endpoint.
// +optional
// +kubebuilder:validation:Pattern=`^https://[^[:space:]]+$`
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="loginURL is immutable"
LoginURL string `json:"loginURL,omitempty"`

// AuthKeySecretRef selects an externally managed bootstrap key.
// It is mutually exclusive with managedTailnetID.
// +optional
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="authKeySecretRef is immutable"
AuthKeySecretRef *corev1.SecretKeySelector `json:"authKeySecretRef,omitempty"`

// Tags are the policy identity requested for the external device.
// +optional
// +listType=set
Tags []string `json:"tags,omitempty"`

// AdvertiseRoutes are the exact prefixes served by this instance.
// +required
// +listType=set
AdvertiseRoutes []string `json:"advertiseRoutes"`

// Image is the stock Tailscale container image.
// +required
Image string `json:"image"`

// Workload is the resolved Pod configuration.
// +optional
Workload ConnectorWorkloadSpec `json:"workload,omitempty"`
}

// ConnectorInstanceStatus records the external child currently associated
// with one Pod incarnation. It never contains tailscaled private state.
type ConnectorInstanceStatus struct {
// ObservedGeneration is the latest reconciled generation.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`

// PodName is the controller-owned Pod for this incarnation.
// +optional
PodName string `json:"podName,omitempty"`

// PodUID disambiguates a recreated Pod with the same name.
// +optional
PodUID string `json:"podUID,omitempty"`

// RequestedHostname is the unique correlation key presented to the
// control plane during registration.
// +optional
RequestedHostname string `json:"requestedHostname,omitempty"`

// Device is the last external control-plane observation.
// +optional
Device ConnectorDeviceStatus `json:"device,omitempty"`

// Conditions reports provisioning and route readiness.
// +optional
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName={"kconnectorinstance"}
// +kubebuilder:printcolumn:name="CONNECTOR",type="string",JSONPath=".spec.connectorRef.name"
// +kubebuilder:printcolumn:name="SLOT",type="integer",JSONPath=".spec.slot"
// +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status"
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"

// ConnectorInstance is an internal lifecycle record for one replaceable
// Connector device and its Pod.
type ConnectorInstance struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec ConnectorInstanceSpec `json:"spec"`
Status ConnectorInstanceStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// ConnectorInstanceList contains a list of ConnectorInstance.
type ConnectorInstanceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ConnectorInstance `json:"items"`
}

func init() {
SchemeBuilder.Register(&ConnectorInstance{}, &ConnectorInstanceList{})
}
Loading
Loading