Allow to specify GNP to access to a software repo

Sometimes armada isn't able to download the chart due to
restricting network policies in k8s cluster. This patch
allows to specify chart name which contains updated
network policies and temporaty GNP template to fetch
the new network policies and apply them.

Change-Id: I7a9feb2ecd460618d7606bc8a31fdb2e97a31f85
Signed-off-by: Ruslan Aliev <raliev@mirantis.com>
This commit is contained in:
Ruslan Aliev 2025-02-27 17:38:10 -06:00
parent e864c8ed63
commit 19e7fdafe3
4 changed files with 171 additions and 4 deletions

2
go.mod
View File

@ -10,6 +10,7 @@ require (
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/onsi/ginkgo/v2 v2.19.0
github.com/onsi/gomega v1.33.1
github.com/tigera/api v0.0.0-20230406222214-ca74195900cb
helm.sh/helm/v3 v3.16.4
k8s.io/api v0.31.5
k8s.io/apiextensions-apiserver v0.31.5
@ -84,6 +85,7 @@ require (
github.com/huandu/xstrings v1.5.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/copier v0.3.5 // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect

10
go.sum
View File

@ -220,6 +220,8 @@ github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@ -294,6 +296,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
@ -365,6 +371,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tigera/api v0.0.0-20230406222214-ca74195900cb h1:Y7r5Al3V235KaEoAzGBz9RYXEbwDu8CPaZoCq2PlD8w=
github.com/tigera/api v0.0.0-20230406222214-ca74195900cb/go.mod h1:ZZghiX3CUsBAc0osBjRvV6y/eun2ObYdvSbjqXAoj/w=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@ -510,6 +518,8 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=

View File

@ -1,6 +1,6 @@
ARG FROM=quay.io/airshipit/ubuntu:jammy
# Build the manager binary
FROM quay.io/airshipit/golang:1.23.1-bullseye as builder
FROM quay.io/airshipit/golang:1.23.1-bullseye AS builder
ARG TARGETOS
ARG TARGETARCH

View File

@ -22,15 +22,20 @@ import (
"errors"
"fmt"
"io"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"net"
"net/http"
"net/url"
"os"
"slices"
"strings"
"time"
"github.com/go-logr/logr"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/go-retryablehttp"
v3 "github.com/tigera/api/pkg/apis/projectcalico/v3"
calico "github.com/tigera/api/pkg/client/clientset_generated/clientset"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
@ -40,12 +45,15 @@ import (
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
apiextension "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
apierrors "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
@ -468,10 +476,38 @@ func (r *ArmadaChartReconciler) loadHelmChart(ctx context.Context, hr armadav1.A
return nil, fmt.Errorf("failed to create a new request: %w", err)
}
resp, err := r.httpClient.Do(req)
var resp *http.Response
resp, err = r.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to download artifact, error: %w", err)
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
log.Info(fmt.Sprintf("failed to download artifact due to %s, attempting to apply temporary GNP", err.Error())) // remove
urlInfo, err := url.Parse(source)
if err != nil {
return nil, err
}
hostname := strings.TrimPrefix(urlInfo.Hostname(), "www.")
ips, err := net.LookupIP(hostname)
if err != nil {
return nil, err
}
var nets []string
for _, ip := range ips {
nets = append(nets, fmt.Sprintf("%s/32", ip.String()))
}
resp, err = r.downloadWithGNP(hr.Spec.ChartName, req, nets, log)
if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("failed to download artifact, error: %w", err)
}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
@ -486,6 +522,125 @@ func (r *ArmadaChartReconciler) loadHelmChart(ctx context.Context, hr armadav1.A
return loader.Load(f.Name())
}
func (r *ArmadaChartReconciler) getGNPTemplate(chartName string, log logr.Logger) ([]byte, error) {
operatorNamespace := os.Getenv("NAMESPACE")
configGetter, err := r.buildRESTClientGetter(operatorNamespace, log)
if err != nil {
return nil, err
}
restConfig, err := configGetter.ToRESTConfig()
if err != nil {
return nil, err
}
clientSet, err := kubernetes.NewForConfig(restConfig)
if err != nil {
return nil, err
}
configMap, err := clientSet.CoreV1().ConfigMaps(operatorNamespace).Get(context.TODO(), "armada-gnp", v1.GetOptions{})
if err != nil {
return nil, err
}
if val, ok := configMap.Data[chartName]; ok {
return []byte(val), nil
}
return nil, errors.New(fmt.Sprintf("failed to download chart %s, no GNP policy found", chartName))
}
func (r *ArmadaChartReconciler) getGNP(chartName string, log logr.Logger) (*v3.GlobalNetworkPolicy, error) {
sch := runtime.NewScheme()
if err := v3.AddToScheme(sch); err != nil {
return nil, err
}
cfgMapData, err := r.getGNPTemplate(chartName, log)
if err != nil {
return nil, err
}
obj, _, err := serializer.NewCodecFactory(sch).UniversalDeserializer().Decode(cfgMapData, nil, nil)
if err != nil {
return nil, err
}
return obj.(*v3.GlobalNetworkPolicy), nil
}
func (r *ArmadaChartReconciler) downloadWithGNP(chartName string, req *retryablehttp.Request, nets []string, log logr.Logger) (*http.Response, error) {
armadaGNP, err := r.getGNP(chartName, log)
if err != nil {
return nil, err
}
clientGetter, err := r.buildRESTClientGetter("default", log)
if err != nil {
return nil, err
}
restConfig, err := clientGetter.ToRESTConfig()
if err != nil {
return nil, err
}
calicoClient, err := calico.NewForConfig(restConfig)
if err != nil {
return nil, err
}
// consider using wait.PollUntilContextTimeout
deleteGNP := func(e error) error {
return errors.Join(e, calicoClient.ProjectcalicoV3().GlobalNetworkPolicies().Delete(context.TODO(), armadaGNP.Name, v1.DeleteOptions{}))
}
appendNets := func(ruleType string, rules *[]v3.Rule) {
for i, rule := range *rules {
for _, n := range nets {
if !slices.Contains(rule.Destination.Nets, n) {
log.Info(fmt.Sprintf("appending net %s to the %s rule policy type", n, ruleType))
(*rules)[i].Destination.Nets = append((*rules)[i].Destination.Nets, n)
}
}
}
}
addNetsToRule := func(ruleType string) {
switch ruleType {
case "Egress":
appendNets(ruleType, &armadaGNP.Spec.Egress)
case "Ingress":
appendNets(ruleType, &armadaGNP.Spec.Ingress)
}
}
for _, policyType := range armadaGNP.Spec.Types {
addNetsToRule(string(policyType))
}
oldGNP, err := calicoClient.ProjectcalicoV3().GlobalNetworkPolicies().Get(context.TODO(), armadaGNP.Name, v1.GetOptions{})
if err == nil && oldGNP != nil {
log.Info("armada gnp already exists, deleting")
err = deleteGNP(nil)
if err != nil {
return nil, err
}
}
_, err = calicoClient.ProjectcalicoV3().GlobalNetworkPolicies().Create(context.TODO(), armadaGNP, v1.CreateOptions{})
if err != nil {
return nil, err
}
log.Info("armada gnp has been created, waiting 5 second for the rules to apply")
time.Sleep(5 * time.Second)
resp, err := r.httpClient.Do(req)
if err != nil {
return nil, deleteGNP(err)
}
return resp, deleteGNP(nil)
}
func (r *ArmadaChartReconciler) buildRESTClientGetter(namespace string, l logr.Logger) (genericclioptions.RESTClientGetter, error) {
opts := []kube.Option{
kube.WithNamespace(namespace),