Skip to content

Commit 3e103af

Browse files
committed
MAJOR: crd: add job for custom resource definition handling
CRDs are not properly handled with external tools, Helm and similar options cannot handle upgrading of custom resource definitions
1 parent dce3c3d commit 3e103af

18 files changed

+333
-16
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ kubernetes-ingress
44
dist/
55
.code-generator/
66
bin/golangci-lint
7+
.local/*

crs/definition/embed.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package definition
2+
3+
import _ "embed"
4+
5+
//go:embed defaults.core.haproxy.org.yaml
6+
var DefaultsV1alpha2 []byte
7+
8+
//go:embed globals.core.haproxy.org.yaml
9+
var GlobalsV1alpha2 []byte
10+
11+
//go:embed backends.core.haproxy.org.yaml
12+
var BackendsV1alpha2 []byte
13+
14+
//go:embed upgrade/defaults.core.haproxy.org.yaml
15+
var DefaultsV1alpha1V1alpha2 []byte
16+
17+
//go:embed upgrade/globals.core.haproxy.org.yaml
18+
var GlobalsV1alpha1V1alpha2 []byte
19+
20+
//go:embed upgrade/backends.core.haproxy.org.yaml
21+
var BackendsV1alpha1V1alpha2 []byte
22+
23+
func GetCRDs() map[string][]byte {
24+
return map[string][]byte{
25+
"defaults.core.haproxy.org": DefaultsV1alpha2,
26+
"globals.core.haproxy.org": GlobalsV1alpha2,
27+
"backends.core.haproxy.org": BackendsV1alpha2,
28+
}
29+
}
30+
31+
func GetCRDsUpgrade() map[string][]byte {
32+
return map[string][]byte{
33+
"defaults.core.haproxy.org": DefaultsV1alpha1V1alpha2,
34+
"globals.core.haproxy.org": GlobalsV1alpha1V1alpha2,
35+
"backends.core.haproxy.org": BackendsV1alpha1V1alpha2,
36+
}
37+
}

deploy/tests/config/crd/job-crd.yaml

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: batch/v1
2+
kind: Job
3+
metadata:
4+
name: haproxy-ingress-crd # each deploymnent should have a unique name, in example we always recreate the custer
5+
namespace: haproxy-controller
6+
spec:
7+
template:
8+
spec:
9+
serviceAccountName: haproxy-kubernetes-ingress-crd
10+
containers:
11+
- name: haproxy-ingress-crd
12+
image: haproxytech/kubernetes-ingress:latest
13+
imagePullPolicy: Never
14+
command: ["./haproxy-ingress-controller","--job-check-crd"]
15+
restartPolicy: Never
16+
backoffLimit: 0

deploy/tests/config/crd/rbac.yaml

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
apiVersion: v1
3+
kind: ServiceAccount
4+
metadata:
5+
name: haproxy-kubernetes-ingress-crd
6+
namespace: haproxy-controller
7+
---
8+
kind: ClusterRole
9+
apiVersion: rbac.authorization.k8s.io/v1
10+
metadata:
11+
name: haproxy-kubernetes-ingress-crd
12+
rules:
13+
- apiGroups:
14+
- "apiextensions.k8s.io"
15+
resources:
16+
- customresourcedefinitions
17+
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
18+
---
19+
kind: ClusterRoleBinding
20+
apiVersion: rbac.authorization.k8s.io/v1
21+
metadata:
22+
name: haproxy-kubernetes-ingress-crd
23+
namespace: haproxy-controller
24+
roleRef:
25+
apiGroup: rbac.authorization.k8s.io
26+
kind: ClusterRole
27+
name: haproxy-kubernetes-ingress-crd
28+
subjects:
29+
- kind: ServiceAccount
30+
name: haproxy-kubernetes-ingress-crd
31+
namespace: haproxy-controller

deploy/tests/create.sh

+6-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ if [ -n "${CI_ENV}" ]; then
1212
echo "building image for ingress controller"
1313
if [ -n "${GITLAB_CI}" ]; then
1414
echo "haproxytech/kubernetes-ingress image already available from previous stage"
15-
#docker build --build-arg TARGETPLATFORM="linux/amd64" -t haproxytech/kubernetes-ingress -f build/Dockerfile .
1615
else
1716
docker build --build-arg TARGETPLATFORM="linux/amd64" -t haproxytech/kubernetes-ingress -f build/Dockerfile .
1817
fi
@@ -46,11 +45,14 @@ fi
4645
echo "loading image http-echo in kind"
4746
kind load docker-image haproxytech/http-echo:latest --name=$clustername
4847

48+
printf %80s |tr " " "="; echo ""
49+
echo "Create HAProxy namespace ..."
50+
kubectl apply -f $DIR/config/0.namespace.yaml
4951
printf %80s |tr " " "="; echo ""
5052
echo "Install custom resource definitions ..."
51-
kubectl apply -f $DIR/../../crs/definition/backend.yaml
52-
kubectl apply -f $DIR/../../crs/definition/defaults.yaml
53-
kubectl apply -f $DIR/../../crs/definition/global.yaml
53+
kubectl apply -f $DIR/../../deploy/tests/config/crd/rbac.yaml
54+
kubectl apply -f $DIR/../../deploy/tests/config/crd/job-crd.yaml
55+
kubectl wait --for=condition=complete --timeout=60s job/haproxy-ingress-crd -n haproxy-controller
5456

5557
if [ "$EXPERIMENTAL_GWAPI" = "1" ]; then
5658
printf %80s |tr " " "="; echo ""
@@ -80,7 +82,6 @@ fi
8082

8183
printf %80s |tr " " "="; echo ""
8284
echo "deploying Ingress Controller ..."
83-
kubectl apply -f $DIR/config/0.namespace.yaml
8485
kubectl apply -f $DIR/config/1.rbac.yaml
8586
if [ "$EXPERIMENTAL_GWAPI" = "1" ]; then
8687
printf %80s |tr " " "="; echo ""

go.mod

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ require (
2121
k8s.io/client-go v0.28.1
2222
sigs.k8s.io/controller-runtime v0.16.2
2323
sigs.k8s.io/gateway-api v0.5.0
24+
sigs.k8s.io/yaml v1.4.0
2425
)
2526

2627
require (
@@ -87,6 +88,5 @@ require (
8788
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
8889
k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect
8990
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
90-
sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect
91-
sigs.k8s.io/yaml v1.3.0 // indirect
91+
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
9292
)

go.sum

+4-4
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ sigs.k8s.io/gateway-api v0.5.0 h1:ze+k9fJqvmL8s1t3e4q1ST8RnN+f09dEv+gfacahlAE=
489489
sigs.k8s.io/gateway-api v0.5.0/go.mod h1:x0AP6gugkFV8fC/oTlnOMU0pnmuzIR8LfIPRVUjxSqA=
490490
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
491491
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
492-
sigs.k8s.io/structured-merge-diff/v4 v4.3.0 h1:UZbZAZfX0wV2zr7YZorDz6GXROfDFj6LvqCRm4VUVKk=
493-
sigs.k8s.io/structured-merge-diff/v4 v4.3.0/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
494-
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
495-
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
492+
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
493+
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
494+
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
495+
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

main.go

+17
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434

3535
"github.com/haproxytech/kubernetes-ingress/pkg/annotations"
3636
"github.com/haproxytech/kubernetes-ingress/pkg/controller"
37+
"github.com/haproxytech/kubernetes-ingress/pkg/job"
3738
"github.com/haproxytech/kubernetes-ingress/pkg/k8s"
3839
"github.com/haproxytech/kubernetes-ingress/pkg/store"
3940
"github.com/haproxytech/kubernetes-ingress/pkg/utils"
@@ -70,6 +71,22 @@ func main() {
7071
parser.WriteHelp(os.Stdout)
7172
return
7273
}
74+
if osArgs.JobCheckCRD {
75+
logger.Print(IngressControllerInfo)
76+
logger.Print(job.IngressControllerCRDUpdater)
77+
logger.Infof("HAProxy Ingress Controller CRD Updater %s %s%s", GitTag, GitCommit, GitDirty)
78+
logger.Infof("Build from: %s", GitRepo)
79+
logger.Infof("Build date: %s\n", GitCommitDate)
80+
81+
err := job.CRDRefresh(logger, osArgs)
82+
if err != nil {
83+
logger.Error(err)
84+
os.Exit(1) //nolint:gocritic
85+
}
86+
// exit, this is just a job
87+
os.Exit(0)
88+
}
89+
7390
logger.ShowFilename(false)
7491
exit := logInfo(logger, osArgs)
7592
if exit {

pkg/job/crd-check.go

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright 2023 HAProxy Technologies LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package job
15+
16+
import (
17+
"context"
18+
19+
"github.com/haproxytech/kubernetes-ingress/crs/definition"
20+
"github.com/haproxytech/kubernetes-ingress/pkg/k8s"
21+
"github.com/haproxytech/kubernetes-ingress/pkg/utils"
22+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
23+
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
24+
apiError "k8s.io/apimachinery/pkg/api/errors"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"sigs.k8s.io/yaml"
27+
)
28+
29+
func CRDRefresh(log utils.Logger, osArgs utils.OSArgs) error {
30+
log.Info("checking CRDS")
31+
config, err := k8s.GetRestConfig(osArgs)
32+
if err != nil {
33+
return err
34+
}
35+
36+
// Create a new clientset for the apiextensions API group
37+
clientset := apiextensionsclientset.NewForConfigOrDie(config)
38+
39+
// Check if the CRD exists
40+
crds := definition.GetCRDs()
41+
crdsUpgrade := definition.GetCRDsUpgrade()
42+
for crdName, crdDef := range crds {
43+
// CustomResourceDefinition object
44+
var crd apiextensionsv1.CustomResourceDefinition
45+
err = yaml.Unmarshal(crdDef, &crd)
46+
if err != nil {
47+
return err
48+
}
49+
log.Info("")
50+
log.Infof("checking CRD %s", crdName)
51+
52+
existingVersion, err := clientset.ApiextensionsV1().CustomResourceDefinitions().Get(context.Background(), crdName, metav1.GetOptions{})
53+
if err != nil {
54+
if !apiError.IsNotFound(err) {
55+
return err
56+
}
57+
log.Infof("CRD %s does not exist", crdName)
58+
// Create the CRD
59+
_, err = clientset.ApiextensionsV1().CustomResourceDefinitions().Create(context.Background(), &crd, metav1.CreateOptions{})
60+
if err != nil {
61+
return err
62+
}
63+
log.Infof("CRD %s created", crdName)
64+
continue
65+
}
66+
log.Infof("CRD %s exists", crdName)
67+
versions := existingVersion.Spec.Versions
68+
if len(versions) == 2 {
69+
log.Infof("CRD %s exists as v1alpha1 and v1alpha2, nothing to do", crdName)
70+
continue
71+
}
72+
// check if we have alpha 2 or we need to upgrade for alpha2
73+
crd.ObjectMeta.ResourceVersion = existingVersion.ObjectMeta.ResourceVersion
74+
if versions[0].Name == "v1alpha2" {
75+
log.Infof("CRD %s exists as v1alpha2, nothing to do", crdName)
76+
continue
77+
}
78+
err = yaml.Unmarshal(crdsUpgrade[crdName], &crd)
79+
if err != nil {
80+
return err
81+
}
82+
// Upgrade the CRDl
83+
_, err = clientset.ApiextensionsV1().CustomResourceDefinitions().Update(context.Background(), &crd, metav1.UpdateOptions{})
84+
if err != nil {
85+
return err
86+
}
87+
}
88+
89+
log.Info("")
90+
log.Info("CRD update done")
91+
return nil
92+
}
93+
94+
// IngressControllerCRDUpdater console pretty print
95+
const IngressControllerCRDUpdater = `
96+
____ ____ ____ _ _ _ _
97+
/ ___| _ \| _ \ | | | |_ __ __| | __ _| |_ ___ _ __
98+
| | | |_) | | | | | | | | '_ \ / _` + "`" + ` |/ _` + "`" + ` | __/ _ \ '__|
99+
| |___| _ <| |_| | | |_| | |_) | (_| | (_| | || __/ |
100+
\____|_| \_\____/ \___/| .__/ \__,_|\__,_|\__\___|_|
101+
|_|
102+
103+
`

pkg/k8s/crs-monitor.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2019 HAProxy Technologies LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package k8s
16+
17+
import (
18+
"strings"
19+
"time"
20+
21+
"k8s.io/client-go/tools/cache"
22+
23+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
24+
// apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
25+
apiextensionsinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
26+
)
27+
28+
func (k k8s) runCRDefinitionsInformer(eventChan chan string, stop chan struct{}) { //nolint:ireturn
29+
// Create a new informer factory with the clientset.
30+
31+
factory := apiextensionsinformers.NewSharedInformerFactoryWithOptions(k.apiExtensionsClient, k.cacheResyncPeriod)
32+
informer := factory.Apiextensions().V1().CustomResourceDefinitions().Informer()
33+
_, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
34+
AddFunc: func(obj interface{}) {
35+
crd := obj.(*apiextensionsv1.CustomResourceDefinition)
36+
if crd.Spec.Group != "core.haproxy.org" {
37+
return
38+
}
39+
if !(crd.Spec.Names.Kind == "Global" || crd.Spec.Names.Kind == "Defaults" || crd.Spec.Names.Kind == "Backend") {
40+
return
41+
}
42+
for _, version := range crd.Spec.Versions {
43+
if version.Name == "v1alpha2" {
44+
time.Sleep(time.Second * 5) // a little delay is needed to let CRD API be created
45+
eventChan <- crd.Spec.Names.Kind
46+
return
47+
}
48+
}
49+
},
50+
})
51+
52+
go informer.Run(stop)
53+
54+
if !cache.WaitForCacheSync(stop, informer.HasSynced) {
55+
logger.Error("Caches are not populated due to an underlying error, cannot monitor CRS creation")
56+
}
57+
58+
logger.Error(err)
59+
}
60+
61+
func (k k8s) RunCRSCreationMonitoring(eventChan chan SyncDataEvent, stop chan struct{}) {
62+
count := 0
63+
for key := range k.crs {
64+
if strings.Contains(key, "core.haproxy.org/v1alpha2") {
65+
count++
66+
}
67+
}
68+
if count > 2 {
69+
// all crds are already in list
70+
return
71+
}
72+
73+
eventCRS := make(chan string)
74+
k.runCRDefinitionsInformer(eventCRS, stop)
75+
go func(chan string) {
76+
for {
77+
select {
78+
case crdName := <-eventCRS:
79+
if _, ok := k.crs["core.haproxy.org/v1alpha2 - "+crdName]; ok {
80+
// we have already created watchers for this CRD
81+
continue
82+
}
83+
informersSyncedEvent := &[]cache.InformerSynced{}
84+
for _, namespace := range k.whiteListedNS {
85+
crs := map[string]CR{}
86+
switch crdName {
87+
case "Backend":
88+
crs[crdName] = NewBackendCR()
89+
case "Defaults":
90+
crs[crdName] = NewDefaultsCR()
91+
case "Global":
92+
crs[crdName] = NewGlobalCR()
93+
}
94+
logger.Info("Custom resource definition created, adding CR watcher for " + crs[crdName].GetKind())
95+
k.runCRInformers(eventChan, stop, namespace, informersSyncedEvent, crs)
96+
}
97+
98+
if !cache.WaitForCacheSync(stop, *informersSyncedEvent...) {
99+
logger.Error("Caches are not populated due to an underlying error, cannot monitor new CRDs")
100+
}
101+
case <-stop:
102+
return
103+
}
104+
}
105+
}(eventCRS)
106+
}

0 commit comments

Comments
 (0)