Skip to content

Allow running outside a cluster #305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 15, 2025
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
2 changes: 1 addition & 1 deletion cmd/caddy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
// createApiserverClient creates a new Kubernetes REST client. We assume the
// controller runs inside Kubernetes and use the in-cluster config.
func createApiserverClient(logger *zap.SugaredLogger) (*kubernetes.Clientset, *version.Info, error) {
cfg, err := clientcmd.BuildConfigFromFlags("", "")
cfg, err := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG"))

Check warning on line 76 in cmd/caddy/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/caddy/main.go#L76

Added line #L76 was not covered by tests
if err != nil {
return nil, nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/caddy/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestConvertToCaddyConfig(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cfg, err := Converter{}.ConvertToCaddyConfig(store.NewStore(store.Options{}, &store.PodInfo{}))
cfg, err := Converter{}.ConvertToCaddyConfig(store.NewStore(store.Options{}, "", &store.PodInfo{}))
require.NoError(t, err)

cfgJSON, err := json.Marshal(cfg)
Expand Down
2 changes: 1 addition & 1 deletion internal/caddy/global/secrets_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
config.Storage = converter.Storage{
System: "secret_store",
StorageValues: converter.StorageValues{
Namespace: store.CurrentPod.Namespace,
Namespace: store.ConfigNamespace,

Check warning on line 25 in internal/caddy/global/secrets_store.go

View check run for this annotation

Codecov / codecov/patch

internal/caddy/global/secrets_store.go#L25

Added line #L25 was not covered by tests
LeaseID: store.Options.LeaseID,
},
}
Expand Down
2 changes: 1 addition & 1 deletion internal/caddy/global/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (p TLSPlugin) GlobalHandler(config *converter.Config, store *store.Store) e
}

if len(hosts) > 0 {
tlsApp.CertificatesRaw["load_folders"] = json.RawMessage(`["` + controller.CertFolder + `"]`)
tlsApp.CertificatesRaw["load_folders"] = json.RawMessage(`["` + controller.GetCertFolder() + `"]`)
// do not manage certificates for those hosts
httpServer.AutoHTTPS.SkipCerts = hosts
}
Expand Down
2 changes: 1 addition & 1 deletion internal/caddy/global/tls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ func TestIngressTlsSkipCertificates(t *testing.T) {
t.Run(tC.desc, func(t *testing.T) {
tp := TLSPlugin{}
c := converter.NewConfig()
s := store.NewStore(store.Options{}, &store.PodInfo{})
s := store.NewStore(store.Options{}, "", &store.PodInfo{})

for _, ing := range tC.ingresses {
s.AddIngress(ing)
Expand Down
31 changes: 21 additions & 10 deletions internal/controller/action_tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,21 @@
apiv1 "k8s.io/api/core/v1"
)

var CertFolder = filepath.FromSlash("/etc/caddy/certs")
var certFolder = ""

// GetCertFolder returns the staging path for storing certificates as files.
func GetCertFolder() string {
if certFolder == "" {
// Use the systemd cache directory if possible.
runtimeDir := os.Getenv("RUNTIME_DIRECTORY")
if runtimeDir != "" {
certFolder = filepath.Join(runtimeDir, "certs")
} else {
certFolder = filepath.FromSlash("/etc/caddy/certs")
}

Check warning on line 22 in internal/controller/action_tls.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/action_tls.go#L14-L22

Added lines #L14 - L22 were not covered by tests
}
return certFolder

Check warning on line 24 in internal/controller/action_tls.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/action_tls.go#L24

Added line #L24 was not covered by tests
}

// SecretAddedAction provides an implementation of the action interface.
type SecretAddedAction struct {
Expand Down Expand Up @@ -60,7 +74,7 @@
content = append(content, cert...)
}

err := os.WriteFile(filepath.Join(CertFolder, s.Name+".pem"), content, 0644)
err := os.WriteFile(filepath.Join(GetCertFolder(), s.Name+".pem"), content, 0644)

Check warning on line 77 in internal/controller/action_tls.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/action_tls.go#L77

Added line #L77 was not covered by tests
if err != nil {
return err
}
Expand All @@ -80,13 +94,17 @@

func (r SecretDeletedAction) handle(c *CaddyController) error {
c.logger.Infof("TLS secret deleted (%s/%s)", r.resource.Namespace, r.resource.Name)
return os.Remove(filepath.Join(CertFolder, r.resource.Name+".pem"))
return os.Remove(filepath.Join(GetCertFolder(), r.resource.Name+".pem"))

Check warning on line 97 in internal/controller/action_tls.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/action_tls.go#L97

Added line #L97 was not covered by tests
}

// watchTLSSecrets Start listening to TLS secrets if at least one ingress needs it.
// It will sync the CertFolder with TLS secrets
func (c *CaddyController) watchTLSSecrets() error {
if c.informers.TLSSecret == nil && c.resourceStore.HasManagedTLS() {
if err := os.MkdirAll(GetCertFolder(), 0755); err != nil && !os.IsExist(err) {
return err
}

Check warning on line 106 in internal/controller/action_tls.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/action_tls.go#L104-L106

Added lines #L104 - L106 were not covered by tests

// Init informers
params := k8s.TLSSecretParams{
InformerFactory: c.factories.WatchedNamespace,
Expand All @@ -106,13 +124,6 @@
return err
}

if _, err := os.Stat(CertFolder); os.IsNotExist(err) {
err = os.MkdirAll(CertFolder, 0755)
if err != nil {
return err
}
}

for _, secret := range secrets {
if err := writeFile(secret); err != nil {
return err
Expand Down
28 changes: 19 additions & 9 deletions internal/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"encoding/json"
"fmt"
"os"
"strings"
"time"

"github.com/caddyserver/caddy/v2"
Expand Down Expand Up @@ -52,10 +53,10 @@

// InformerFactory contains shared informer factory
// We need to type of factory:
// - One used to watch resources in the Pod namespaces (caddy config, secrets...)
// - One used to watch ConfigMap and Secret resources
// - Another one for Ingress resources in the selected namespace
type InformerFactory struct {
PodNamespace informers.SharedInformerFactory
ConfigNamespace informers.SharedInformerFactory
WatchedNamespace informers.SharedInformerFactory
}

Expand Down Expand Up @@ -105,16 +106,25 @@
factories: &InformerFactory{},
}

podInfo, err := k8s.GetPodDetails(kubeClient)
podInfo, err := k8s.GetPodDetails(logger, kubeClient)

Check warning on line 109 in internal/controller/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/controller.go#L109

Added line #L109 was not covered by tests
if err != nil {
logger.Fatalf("Unexpected error obtaining pod information: %v", err)
}

var configNamespace, configMapName string
if parts := strings.SplitN(opts.ConfigMapName, "/", 2); len(parts) == 2 {
configNamespace, configMapName = parts[0], parts[1]
} else if podInfo != nil {
configNamespace, configMapName = podInfo.Namespace, opts.ConfigMapName
} else {
logger.Fatalf("Must set a namespace for -config-map when running outside a cluster: %s", opts.ConfigMapName)
}

Check warning on line 121 in internal/controller/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/controller.go#L114-L121

Added lines #L114 - L121 were not covered by tests

// Create informer factories
controller.factories.PodNamespace = informers.NewSharedInformerFactoryWithOptions(
controller.factories.ConfigNamespace = informers.NewSharedInformerFactoryWithOptions(

Check warning on line 124 in internal/controller/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/controller.go#L124

Added line #L124 was not covered by tests
kubeClient,
resourcesSyncInterval,
informers.WithNamespace(podInfo.Namespace),
informers.WithNamespace(configNamespace),

Check warning on line 127 in internal/controller/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/controller.go#L127

Added line #L127 was not covered by tests
)
controller.factories.WatchedNamespace = informers.NewSharedInformerFactoryWithOptions(
kubeClient,
Expand All @@ -136,9 +146,9 @@

// Watch Configmap in the pod's namespace for global options
cmOptionsParams := k8s.ConfigMapParams{
Namespace: podInfo.Namespace,
InformerFactory: controller.factories.PodNamespace,
ConfigMapName: opts.ConfigMapName,
Namespace: configNamespace,
InformerFactory: controller.factories.ConfigNamespace,
ConfigMapName: configMapName,

Check warning on line 151 in internal/controller/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/controller.go#L149-L151

Added lines #L149 - L151 were not covered by tests
}
controller.informers.ConfigMap = k8s.WatchConfigMaps(cmOptionsParams, k8s.ConfigMapHandlers{
AddFunc: controller.onConfigMapAdded,
Expand All @@ -147,7 +157,7 @@
})

// Create resource store
controller.resourceStore = store.NewStore(opts, podInfo)
controller.resourceStore = store.NewStore(opts, configNamespace, podInfo)

Check warning on line 160 in internal/controller/controller.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/controller.go#L160

Added line #L160 was not covered by tests

return controller
}
Expand Down
13 changes: 12 additions & 1 deletion internal/k8s/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"os"

"github.com/caddyserver/ingress/pkg/store"
"go.uber.org/zap"

apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -18,6 +19,11 @@
func GetAddresses(p *store.PodInfo, kubeClient *kubernetes.Clientset) ([]string, error) {
var addrs []string

// If not running inside a cluster, we currently don't report any addresses
if p == nil {
return addrs, nil
}

Check warning on line 25 in internal/k8s/pod.go

View check run for this annotation

Codecov / codecov/patch

internal/k8s/pod.go#L22-L25

Added lines #L22 - L25 were not covered by tests

// Get services that may select this pod
svcs, err := kubeClient.CoreV1().Services(p.Namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
Expand Down Expand Up @@ -78,10 +84,15 @@

// GetPodDetails returns runtime information about the pod:
// name, namespace and IP of the node where it is running
func GetPodDetails(kubeClient *kubernetes.Clientset) (*store.PodInfo, error) {
func GetPodDetails(logger *zap.SugaredLogger, kubeClient *kubernetes.Clientset) (*store.PodInfo, error) {

Check warning on line 87 in internal/k8s/pod.go

View check run for this annotation

Codecov / codecov/patch

internal/k8s/pod.go#L87

Added line #L87 was not covered by tests
podName := os.Getenv("POD_NAME")
podNs := os.Getenv("POD_NAMESPACE")

if podName == "" && podNs == "" {
logger.Warn("POD_NAME and POD_NAMESPACE are not set, assuming the controller is running outside a cluster")
return nil, nil
}

Check warning on line 94 in internal/k8s/pod.go

View check run for this annotation

Codecov / codecov/patch

internal/k8s/pod.go#L91-L94

Added lines #L91 - L94 were not covered by tests

if podName == "" || podNs == "" {
return nil, fmt.Errorf("unable to get POD information (missing POD_NAME or POD_NAMESPACE environment variable")
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"context"
"fmt"
"io/fs"
"os"
"regexp"
"strings"
"time"
Expand Down Expand Up @@ -66,7 +67,7 @@

// Provisions the SecretStorage instance.
func (s *SecretStorage) Provision(ctx caddy.Context) error {
config, _ := clientcmd.BuildConfigFromFlags("", "")
config, _ := clientcmd.BuildConfigFromFlags("", os.Getenv("KUBECONFIG"))

Check warning on line 70 in pkg/storage/storage.go

View check run for this annotation

Codecov / codecov/patch

pkg/storage/storage.go#L70

Added line #L70 was not covered by tests
// creates the clientset
clientset, _ := kubernetes.NewForConfig(config)

Expand Down
20 changes: 11 additions & 9 deletions pkg/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@ import (

// Store contains resources used to generate Caddy config
type Store struct {
Options *Options
ConfigMap *ConfigMapOptions
Ingresses []*v1.Ingress
CurrentPod *PodInfo
Options *Options
Ingresses []*v1.Ingress
ConfigMap *ConfigMapOptions
ConfigNamespace string
CurrentPod *PodInfo
}

// NewStore returns a new store that keeps track of K8S resources needed by the controller.
func NewStore(opts Options, podInfo *PodInfo) *Store {
func NewStore(opts Options, configNamespace string, podInfo *PodInfo) *Store {
s := &Store{
Options: &opts,
Ingresses: []*v1.Ingress{},
ConfigMap: &ConfigMapOptions{},
CurrentPod: podInfo,
Options: &opts,
Ingresses: []*v1.Ingress{},
ConfigMap: &ConfigMapOptions{},
ConfigNamespace: configNamespace,
CurrentPod: podInfo,
}
return s
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/store/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func TestStoreIngresses(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
s := NewStore(Options{}, &PodInfo{})
s := NewStore(Options{}, "", &PodInfo{})
for _, uid := range test.addIngresses {
i := createIngress(uid)
s.AddIngress(&i)
Expand Down Expand Up @@ -164,7 +164,7 @@ func TestStoreReturnIfHasManagedTLS(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
s := NewStore(Options{}, &PodInfo{})
s := NewStore(Options{}, "", &PodInfo{})
for _, i := range test.ingresses {
s.AddIngress(&i)
}
Expand Down
Loading