Skip to content

Commit

Permalink
the cache is not started can not read objects (#17)
Browse files Browse the repository at this point in the history
* mocks

* devcontainer

* main_test
  • Loading branch information
jmnote authored Aug 17, 2024
1 parent 6f0a2f4 commit 619a17a
Show file tree
Hide file tree
Showing 9 changed files with 692 additions and 63 deletions.
25 changes: 25 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/go
{
"name": "Go",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/go:1-1.22-bookworm",
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
}

// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},

// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],

// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "go version",

// Configure tool-specific properties.
// "customizations": {},

// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for more information:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
# https://containers.dev/guide/dependabot

version: 2
updates:
- package-ecosystem: "devcontainers"
directory: "/"
schedule:
interval: weekly
18 changes: 17 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ lint: golangci-lint ## Run golangci-lint linter
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
$(GOLANGCI_LINT) run --fix

.PHONY: checks
checks: test lint docker-build

.PHONY: mocks
mocks: mockgen
rm -rf cmd/mocks
$(MOCKGEN) -destination=cmd/mocks/mockcache.go -package="mocks" sigs.k8s.io/controller-runtime/pkg/cache Cache
$(MOCKGEN) -destination=cmd/mocks/mockmanager.go -package="mocks" sigs.k8s.io/controller-runtime/pkg/manager Manager

##@ Build

.PHONY: build
Expand Down Expand Up @@ -155,13 +164,15 @@ KUBECTL ?= kubectl
KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
GOLANGCI_LINT ?= $(LOCALBIN)/golangci-lint
MOCKGEN ?= $(LOCALBIN)/mockgen

## Tool Versions
KUSTOMIZE_VERSION ?= v5.4.2
CONTROLLER_TOOLS_VERSION ?= v0.15.0
ENVTEST_VERSION ?= release-0.18
GOLANGCI_LINT_VERSION ?= v1.59.1
MOCKGEN_VERSION ?= v0.4.0

.PHONY: kustomize
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
Expand All @@ -183,6 +194,11 @@ golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))

.PHONY: mockgen
mockgen: $(MOCKGEN) ## Download mockgen locally if necessary.
$(MOCKGEN): $(LOCALBIN)
$(call go-install-tool,$(MOCKGEN),go.uber.org/mock/mockgen,$(MOCKGEN_VERSION))

# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
# $1 - target path with name of binary
# $2 - package url which can be installed
Expand Down
65 changes: 19 additions & 46 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ limitations under the License.
package main

import (
"context"
"crypto/tls"
"errors"
"flag"
"fmt"
"os"

// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
"github.com/kuoss/ingress-annotator/controller"
"github.com/kuoss/ingress-annotator/controller/rulesstore"
_ "k8s.io/client-go/plugin/pkg/client/auth"

"k8s.io/apimachinery/pkg/runtime"
Expand All @@ -37,9 +38,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"

"github.com/kuoss/ingress-annotator/controller"
"github.com/kuoss/ingress-annotator/controller/rulesstore"
// +kubebuilder:scaffold:imports
)

Expand All @@ -56,13 +54,18 @@ func init() {
}

func main() {
if err := run(); err != nil {
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), getManagerOptions())
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
if err := run(mgr); err != nil {
setupLog.Error(err, "unable to run the manager")
os.Exit(1)
}
}

func run() error {
func getManagerOptions() ctrl.Options {
var metricsAddr string
var enableLeaderElection bool
var probeAddr string
Expand All @@ -87,13 +90,6 @@ func run() error {

ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))

// if the enable-http2 flag is false (the default), http/2 should be disabled
// due to its vulnerabilities. More specifically, disabling http/2 will
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
// Rapid Reset CVEs. For more information see:
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
// - https://github.com/advisories/GHSA-4374-p667-p6c8

if !enableHTTP2 {
tlsOpts = append(tlsOpts, func(c *tls.Config) {
setupLog.Info("disabling http/2")
Expand All @@ -104,54 +100,32 @@ func run() error {
webhookServer := webhook.NewServer(webhook.Options{
TLSOpts: tlsOpts,
})
fmt.Println(webhookServer)

// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
// More info:
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/metrics/server
// - https://book.kubebuilder.io/reference/metrics.html
metricsServerOptions := metricsserver.Options{
BindAddress: metricsAddr,
SecureServing: secureMetrics,
// TODO(user): TLSOpts is used to allow configuring the TLS config used for the server. If certificates are
// not provided, self-signed certificates will be generated by default. This option is not recommended for
// production environments as self-signed certificates do not offer the same level of trust and security
// as certificates issued by a trusted Certificate Authority (CA). The primary risk is potentially allowing
// unauthorized access to sensitive metrics data. Consider replacing with CertDir, CertName, and KeyName
// to provide certificates, ensuring the server communicates using trusted and secure certificates.
TLSOpts: tlsOpts,
TLSOpts: tlsOpts,
}

if secureMetrics {
// FilterProvider is used to protect the metrics endpoint with authn/authz.
// These configurations ensure that only authorized users and service accounts
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
// https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/metrics/filters#WithAuthenticationAndAuthorization
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
}

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
return ctrl.Options{
Scheme: scheme,
Metrics: metricsServerOptions,
WebhookServer: webhookServer,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "annotator.ingress.kubernetes.io",
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
// when the Manager ends. This requires the binary to immediately end when the
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
// speeds up voluntary leader transitions as the new leader don't have to wait
// LeaseDuration time first.
//
// In the default scaffold provided, the program ends immediately after
// the manager stops, so would be fine to enable this option. However,
// if you are doing or is intended to do any operation such as perform cleanups
// after the manager stops then its usage might be unsafe.
// LeaderElectionReleaseOnCancel: true,
})
if err != nil {
return errors.New("unable to start manager")
}
}

func run(mgr ctrl.Manager) error {
if !mgr.GetCache().WaitForCacheSync(context.Background()) {
return fmt.Errorf("failed to wait for cache sync")
}
ns, exists := os.LookupEnv("POD_NAMESPACE")
if !exists || ns == "" {
return errors.New("POD_NAMESPACE environment variable is not set or is empty")
Expand All @@ -163,8 +137,7 @@ func run() error {
}
rulesStore, err := rulesstore.New(mgr.GetClient(), nn)
if err != nil {
setupLog.Error(err, "unable to start rules store")
os.Exit(1)
return fmt.Errorf("unable to start rules store: %w", err)
}

if err = (&controller.ConfigMapReconciler{
Expand Down
134 changes: 134 additions & 0 deletions cmd/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package main

import (
"crypto/tls"
"flag"
"testing"

"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/config"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/webhook"

"github.com/kuoss/ingress-annotator/cmd/mocks"
"github.com/kuoss/ingress-annotator/controller/fakeclient"
"github.com/kuoss/ingress-annotator/controller/model"
"github.com/kuoss/ingress-annotator/controller/rulesstore/mockrulesstore"
)

func TestGetManagerOptions(t *testing.T) {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
metricsAddr := fs.String("metrics-bind-address", "0", "")
probeAddr := fs.String("health-probe-bind-address", ":8081", "")
enableLeaderElection := fs.Bool("leader-elect", false, "")
secureMetrics := fs.Bool("metrics-secure", true, "")
enableHTTP2 := fs.Bool("enable-http2", false, "")
err := fs.Parse([]string{})
assert.NoError(t, err)

opts := getManagerOptions()
assert.Equal(t, *metricsAddr, opts.Metrics.BindAddress, "Expected metrics bind address to match")
assert.Equal(t, *probeAddr, opts.HealthProbeBindAddress, "Expected health probe bind address to match")
assert.Equal(t, *enableLeaderElection, opts.LeaderElection, "Expected leader election setting to match")
assert.Equal(t, *secureMetrics, opts.Metrics.SecureServing, "Expected secure metrics setting to match")

webhookServer, ok := opts.WebhookServer.(*webhook.DefaultServer)
if !ok || webhookServer == nil {
t.Fatal("Expected a valid webhook.Server instance")
}

// If enableHTTP2 is false, check the TLS options indirectly
if !*enableHTTP2 {
tlsConfig := &tls.Config{}
for _, tlsOpt := range webhookServer.Options.TLSOpts {
tlsOpt(tlsConfig)
}
assert.Equal(t, []string{"http/1.1"}, tlsConfig.NextProtos, "Expected HTTP/2 to be disabled")
}

// Check the default leader election ID
assert.Equal(t, "annotator.ingress.kubernetes.io", opts.LeaderElectionID, "Expected leader election ID to match")
}

func setupMockManager(mockCtrl *gomock.Controller) (*mocks.MockManager, *mocks.MockCache) {
mockManager := mocks.NewMockManager(mockCtrl)
mockCache := mocks.NewMockCache(mockCtrl)

scheme := fakeclient.NewScheme()

fakeClient := fakeclient.NewClient(nil, &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: "test-namespace",
Name: "ingress-annotator",
},
})

mockManager.EXPECT().GetCache().Return(mockCache).AnyTimes()
mockManager.EXPECT().GetClient().Return(fakeClient).AnyTimes()
mockManager.EXPECT().GetScheme().Return(scheme).AnyTimes()
mockManager.EXPECT().GetControllerOptions().Return(config.Controller{}).AnyTimes()
mockManager.EXPECT().AddHealthzCheck(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mockManager.EXPECT().AddReadyzCheck(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mockManager.EXPECT().GetLogger().Return(zap.New(zap.WriteTo(nil))).AnyTimes()

return mockManager, mockCache
}

func TestRun_PODNamespaceNotSet(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

mockManager, mockCache := setupMockManager(mockCtrl)

mockCache.EXPECT().WaitForCacheSync(gomock.Any()).Return(true).Times(1)

t.Setenv("POD_NAMESPACE", "")
err := run(mockManager)
assert.Error(t, err)
assert.Equal(t, "POD_NAMESPACE environment variable is not set or is empty", err.Error())
}

func TestRun_WaitForCacheSyncReturnsFalse(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

mockManager, mockCache := setupMockManager(mockCtrl)

mockCache.EXPECT().WaitForCacheSync(gomock.Any()).Return(false).Times(1)

t.Setenv("POD_NAMESPACE", "test-namespace")
err := run(mockManager)
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to wait for cache sync")
}

func TestRun_SuccessfulRun(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

mockManager, mockCache := setupMockManager(mockCtrl)

mockCache.EXPECT().WaitForCacheSync(gomock.Any()).Return(true).Times(1)
mockManager.EXPECT().Add(gomock.Any()).Return(nil).AnyTimes()

mockRulesStore := new(mockrulesstore.RulesStore)
rules := &model.Rules{
"default/example-ingress": {
Namespace: "default",
Ingress: "example-ingress",
Annotations: map[string]string{
"new-key": "new-value",
},
},
}
mockRulesStore.On("GetRules").Return(rules)

mockManager.EXPECT().Start(gomock.Any()).Return(nil).Times(1)

t.Setenv("POD_NAMESPACE", "test-namespace")
err := run(mockManager)
assert.NoError(t, err)
}
Loading

0 comments on commit 619a17a

Please sign in to comment.