Skip to content

Commit

Permalink
add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
robertlestak committed Jul 13, 2024
1 parent 025c8d6 commit 938175e
Show file tree
Hide file tree
Showing 25 changed files with 1,333 additions and 33 deletions.
2 changes: 2 additions & 0 deletions .env-sample
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
AWS_SDK_LOAD_CONFIG=1
INCAPSULA_API=https://my.imperva.com/api/prov/v1
SECRETS_NAMESPACE=
DISABLED_NAMESPACES=
ENABLED_NAMESPACES=
OPERATOR_NAME=cert-manager-sync.lestak.sh
LOG_LEVEL=info
CACHE_DISABLE=false
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ WORKDIR /app

COPY . .

RUN go mod download && go mod verify

RUN go test ./...

RUN CGO_ENABLED=0 go build -o /app/cert-manager-sync cmd/cert-manager-sync/*.go
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,9 @@ The operator can be deployed as-is with no additional configuration. However, yo

```bash
INCAPSULA_API=https://my.imperva.com/api/prov/v1 # Incapsula API URL
SECRETS_NAMESPACE= # Namespace to look for secrets in. default to all namespaces
DISABLED_NAMESPACES= # csv of namespaces to ignore. default is empty (all namespaces are watched)
ENABLED_NAMESPACES= # csv of namespaces to watch. default is empty (all namespaces are watched)
SECRETS_NAMESPACE= # DEPRECATED, replaced by ENABLED_NAMESPACES. Namespace to look for secrets in. overrides ENABLED_NAMESPACES if set
OPERATOR_NAME=cert-manager-sync.lestak.sh # Operator name. use for white-labeling
LOG_LEVEL=info # Log level. trace, debug, info, warn, error, fatal
CACHE_DISABLE=false # Disable cache
Expand Down
7 changes: 3 additions & 4 deletions cmd/cert-manager-sync/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"cmp"
"os"
"time"

Expand All @@ -14,7 +15,7 @@ import (
)

func init() {
ll, lerr := log.ParseLevel(os.Getenv("LOG_LEVEL"))
ll, lerr := log.ParseLevel(cmp.Or(os.Getenv("LOG_LEVEL"), "info"))
if lerr != nil {
ll = log.InfoLevel
}
Expand All @@ -24,9 +25,7 @@ func init() {
"action": "init",
},
)
if os.Getenv("OPERATOR_NAME") != "" {
state.OperatorName = os.Getenv("OPERATOR_NAME")
}
state.OperatorName = cmp.Or(os.Getenv("OPERATOR_NAME"), state.OperatorName)
cerr := state.CreateKubeClient()
if cerr != nil {
l.Fatal(cerr)
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/heroku/heroku-go/v5 v5.5.0
github.com/prometheus/client_golang v1.4.0
github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.8.4
golang.org/x/oauth2 v0.8.0
google.golang.org/api v0.128.0
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc
Expand Down Expand Up @@ -89,6 +90,7 @@ require (
github.com/oklog/run v1.0.0 // indirect
github.com/pborman/uuid v1.2.0 // indirect
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.9.1 // indirect
github.com/prometheus/procfs v0.0.8 // indirect
Expand Down
11 changes: 6 additions & 5 deletions internal/metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package metrics

import (
"cmp"
"net/http"
"os"

Expand Down Expand Up @@ -28,17 +29,17 @@ func SetFailure(namespace, secret, store string) {
SyncStatus.WithLabelValues(namespace, secret, store, "fail").Set(1)
}

func init() {
InitMetrics()
}

func Serve() {
l := log.WithFields(log.Fields{
"pkg": "metrics",
"fn": "Serve",
})
l.Debug("starting metrics server")
InitMetrics()
port := os.Getenv("METRICS_PORT")
if port == "" {
port = "9090"
}
port := cmp.Or(os.Getenv("METRICS_PORT"), "9090")
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
Expand Down
59 changes: 59 additions & 0 deletions internal/metrics/metrics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package metrics

import (
"net/http"
"os"
"testing"
"time"

"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)

func TestInitMetrics(t *testing.T) {
// Reset the global registry to ensure a clean state
prometheus.DefaultRegisterer = prometheus.NewRegistry()

InitMetrics()

// Check if SyncStatus is registered
metricFamilies, err := prometheus.DefaultGatherer.Gather()
assert.NoError(t, err)
// ensure we have at least one metric family
assert.Greater(t, len(metricFamilies), 0)
// ensure the metric family is the one we expect
for _, mf := range metricFamilies {
if *mf.Name == "cert_manager_sync_status" {
return
}
}
}

func TestServe(t *testing.T) {
// Mock environment variable
os.Setenv("METRICS_PORT", "9091")

// Mock log output to prevent actual logging during tests
logrus.SetOutput(os.Stdout)
logrus.SetLevel(logrus.PanicLevel)

// Start the metrics server in a separate goroutine
go Serve()

// Wait a moment for the server to start
time.Sleep(100 * time.Millisecond)

// Test /healthz endpoint
resp, err := http.Get("http://localhost:9091/healthz")
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)

// Test /metrics endpoint
resp, err = http.Get("http://localhost:9091/metrics")
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)

// Cleanup: reset the environment variable
os.Unsetenv("METRICS_PORT")
}
6 changes: 5 additions & 1 deletion pkg/certmanagersync/certmanagersync.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var (
ErrInvalidStoreType = errors.New("invalid store type")
)

type StoreType string

const (
Expand Down Expand Up @@ -67,7 +71,7 @@ func NewStore(storeType StoreType) (RemoteStore, error) {
case VaultStoreType:
store = &vault.VaultStore{}
default:
return nil, errors.New("invalid store type")
return nil, ErrInvalidStoreType
}
return store, nil
}
Expand Down
148 changes: 148 additions & 0 deletions pkg/certmanagersync/certmanagersync_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package certmanagersync

import (
"errors"
"reflect"
"testing"

"github.com/robertlestak/cert-manager-sync/pkg/state"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestNewStore(t *testing.T) {
tests := []struct {
name string
storeType StoreType
wantErr bool
}{
{
name: "Test ACMStoreType",
storeType: ACMStoreType,
wantErr: false,
},
{
name: "Test CloudflareStoreType",
storeType: CloudflareStoreType,
wantErr: false,
},
{
name: "Test DigitalOceanStoreType",
storeType: DigitalOceanStoreType,
wantErr: false,
},
{
name: "Test FilepathStoreType",
storeType: FilepathStoreType,
wantErr: false,
},
{
name: "Test GCPStoreType",
storeType: GCPStoreType,
wantErr: false,
},
{
name: "Test HerokuStoreType",
storeType: HerokuStoreType,
wantErr: false,
},
{
name: "Test IncapsulaStoreType",
storeType: IncapsulaStoreType,
wantErr: false,
},
{
name: "Test ThreatxStoreType",
storeType: ThreatxStoreType,
wantErr: false,
},
{
name: "Test VaultStoreType",
storeType: VaultStoreType,
wantErr: false,
},
{
name: "Test invalid store type",
storeType: "invalid",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := NewStore(tt.storeType)
if (err != nil) != tt.wantErr {
t.Errorf("NewStore() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.wantErr {
assert.True(t, errors.Is(err, ErrInvalidStoreType))
}
})
}
}

func TestEnabledStores(t *testing.T) {
tests := []struct {
name string
annotations map[string]string
want []StoreType
}{
{
name: "Test with no enabled stores",
annotations: map[string]string{
state.OperatorName + "/sync-enabled": "false",
},
want: nil,
},
{
name: "Test with ACM and Cloudflare enabled",
annotations: map[string]string{
state.OperatorName + "/sync-enabled": "true",
state.OperatorName + "/acm-enabled": "true",
state.OperatorName + "/cloudflare-enabled": "true",
},
want: []StoreType{ACMStoreType, CloudflareStoreType},
},
{
name: "Test with all stores enabled",
annotations: map[string]string{
state.OperatorName + "/sync-enabled": "true",
state.OperatorName + "/acm-enabled": "true",
state.OperatorName + "/cloudflare-enabled": "true",
state.OperatorName + "/digitalocean-enabled": "true",
state.OperatorName + "/filepath-enabled": "true",
state.OperatorName + "/gcp-enabled": "true",
state.OperatorName + "/heroku-enabled": "true",
state.OperatorName + "/incapsula-site-id": "1234",
state.OperatorName + "/threatx-hostname": "example.com",
state.OperatorName + "/vault-addr": "https://vault.example.com",
},
want: []StoreType{
ACMStoreType,
CloudflareStoreType,
DigitalOceanStoreType,
FilepathStoreType,
GCPStoreType,
HerokuStoreType,
IncapsulaStoreType,
ThreatxStoreType,
VaultStoreType,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Annotations: tt.annotations,
},
}
if got := EnabledStores(s); !reflect.DeepEqual(got, tt.want) {
t.Errorf("EnabledStores() = %v, want %v", got, tt.want)
}
})
}
}
45 changes: 43 additions & 2 deletions pkg/state/certmanagersync.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"

"github.com/robertlestak/cert-manager-sync/pkg/tlssecret"
Expand Down Expand Up @@ -138,6 +139,42 @@ func CreateKubeClient() error {
return nil
}

func namespaceDisabled(n string) bool {
// if DISABLED_NAMESPACES is set, don't watch those namespaces
disabledNs := strings.Split(os.Getenv("DISABLED_NAMESPACES"), ",")
for _, ns := range disabledNs {
if ns == n {
return true
}
}
// if DISABLED_NAMESPACES is not set, watch all namespaces
return false
}

func namespaceEnabled(n string) bool {
// SECRETS_NAMESPACE is deprecated and has been replaced by ENABLED_NAMESPACES.
// SECRETS_NAMESPACE will be removed in a future release
// if a single SECRETS_NAMESPACE is set, only watch that namespace
if os.Getenv("SECRETS_NAMESPACE") != "" && n != os.Getenv("SECRETS_NAMESPACE") {
return false
} else if os.Getenv("SECRETS_NAMESPACE") != "" && n == os.Getenv("SECRETS_NAMESPACE") {
return true
}
// if ENABLED_NAMESPACES is set, only watch those namespaces
if os.Getenv("ENABLED_NAMESPACES") != "" {
enabledNs := strings.Split(os.Getenv("ENABLED_NAMESPACES"), ",")
for _, ns := range enabledNs {
if ns == n {
return true
}
}
// if ENABLED_NAMESPACES is set, but the namespace is not in the list, don't watch
return false
}
// if ENABLED_NAMESPACES is not set, watch all namespaces
return true
}

func SecretWatched(s *corev1.Secret) bool {
l := log.WithFields(
log.Fields{
Expand All @@ -148,8 +185,12 @@ func SecretWatched(s *corev1.Secret) bool {
if s.Annotations[OperatorName+"/sync-enabled"] != "true" {
return false
}
if os.Getenv("SECRETS_NAMESPACE") != "" && s.Namespace != os.Getenv("SECRETS_NAMESPACE") {
l.Debugf("skipping secret in different namespace: %s", s.Namespace)
if namespaceDisabled(s.Namespace) {
l.Debug("namespace disabled")
return false
}
if !namespaceEnabled(s.Namespace) {
l.Debug("namespace not enabled")
return false
}
if len(s.Data["tls.crt"]) == 0 || len(s.Data["tls.key"]) == 0 {
Expand Down
Loading

0 comments on commit 938175e

Please sign in to comment.