Skip to content
4 changes: 3 additions & 1 deletion .github/workflows/build-test-push-to-test-registry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ jobs:
id: install

- name: Install helm plugin helm-images
run: helm plugin install https://github.com/nikhilsbhat/helm-images
run: |
helm plugin install https://github.com/nikhilsbhat/helm-images --verify=false || echo "Plugin already installed"
helm plugin list

- name: Install GoReleaser
uses: goreleaser/goreleaser-action@v6
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/build-test-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ jobs:
id: install

- name: Install helm plugin helm-images
run: helm plugin install https://github.com/nikhilsbhat/helm-images
run: |
helm plugin install https://github.com/nikhilsbhat/helm-images --verify=false || echo "Plugin already installed"
helm plugin list

- name: Install GoReleaser
uses: goreleaser/goreleaser-action@v6
Expand Down
179 changes: 174 additions & 5 deletions internal/services/project-operator.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package services

import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strings"
"time"

"github.com/ca-gip/kubi/internal/utils"
cagipv1 "github.com/ca-gip/kubi/pkg/apis/cagip/v1"
"github.com/ca-gip/kubi/pkg/generated/clientset/versioned"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
kubernetes "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
)
Expand Down Expand Up @@ -61,11 +67,6 @@ func projectUpdated(old interface{}, new interface{}) {
createOrUpdateProjectResources(project)
}

func projectDeleted(obj interface{}) {
project := obj.(*cagipv1.Project)
slog.Warn("Operator: a project was deleted, Kubi won't delete anything, please delete the namespace manualy", "namespace", project.Name)
}

func createOrUpdateProjectResources(project *cagipv1.Project) {

if err := generateNamespace(project); err != nil {
Expand Down Expand Up @@ -104,3 +105,171 @@ func createOrUpdateProjectResources(project *cagipv1.Project) {
RoleBindingsCreation.WithLabelValues("ok", project.Name, "rolebindings").Inc()

}

func projectDeleted(obj interface{}) {
project := obj.(*cagipv1.Project)
slog.Info("Operator: a project was deleted, cleaning up associated resources", "namespace", project.Name)
deleteProjectResources(project)
}

func deleteProjectResources(project *cagipv1.Project) {
//verify that project exists in other clusters
// Skip KGB API check if URL is not configured properly (e.g., in tests)
if utils.Config.KgbApiURL != "" && utils.Config.KgbApiURL != "http://localhost:9999" {
err := checkProjectExistsInOtherClusters(project)
if err != nil {
slog.Error("check KGB API failed", "project", project.Name, "error", err)
return
}
} else {
slog.Info("Skipping KGB API check (not configured or test mode)", "project", project.Name)
}

// Check if there are any pods in the namespace
err := checkPodExistsInNamespace(project.Name)
if err != nil {
slog.Error("pods still exist in namespace, cannot delete resources", "namespace", project.Name, "error", err)
return
}

// Delete role bindings first
if err := deleteRoleBindings(project.Name); err != nil {
slog.Error("failed to delete role bindings", "namespace", project.Name, "error", err)
RoleBindingsCreation.WithLabelValues("delete_error", project.Name, "rolebindings").Inc()
} else {
slog.Debug("role bindings deleted", "namespace", project.Name)
RoleBindingsCreation.WithLabelValues("deleted", project.Name, "rolebindings").Inc()
}

// Delete service account
if err := deleteAppServiceAccount(project.Name); err != nil {
slog.Error("failed to delete service account", "namespace", project.Name, "error", err)
ServiceAccountCreation.WithLabelValues("delete_error", project.Name, utils.KubiServiceAccountAppName).Inc()
} else {
slog.Debug("service account deleted", "object", utils.KubiServiceAccountAppName, "namespace", project.Name)
ServiceAccountCreation.WithLabelValues("deleted", project.Name, utils.KubiServiceAccountAppName).Inc()
}

// Delete network policy if enabled
if utils.Config.NetworkPolicy {
if err := deleteNetworkPolicy(project.Name); err != nil {
slog.Error("failed to delete network policy", "namespace", project.Name, "error", err)
NetworkPolicyCreation.WithLabelValues("delete_error", project.Name, utils.KubiDefaultNetworkPolicyName).Inc()
} else {
slog.Debug("network policy deleted", "object", utils.KubiDefaultNetworkPolicyName, "namespace", project.Name)
NetworkPolicyCreation.WithLabelValues("deleted", project.Name, utils.KubiDefaultNetworkPolicyName).Inc()
}
}

// Delete namespace last
if err := deleteNamespace(project.Name); err != nil {
slog.Error("failed to delete namespace", "namespace", project.Name, "error", err)
NamespaceCreation.WithLabelValues("delete_error", project.Name).Inc()
} else {
slog.Debug("namespace deleted", "namespace", project.Name)
NamespaceCreation.WithLabelValues("deleted", project.Name).Inc()
}
}

func checkProjectExistsInOtherClusters(project *cagipv1.Project) error {
//call kgb api https://kgb-api.devops.caas.cagip.group.gca/api/v1/clusters for see if project exists in other clusters
//call kgb api
client := &http.Client{Timeout: 90 * time.Second}
apiURL := fmt.Sprintf("%s/api/v1/clusters", utils.Config.KgbApiURL)
resp, err := client.Get(apiURL)
if err != nil {
return err
}
if resp != nil {
defer resp.Body.Close()
}

if resp.StatusCode != 200 {
return fmt.Errorf("unexpected status code for call KGB API: %d", resp.StatusCode)
}

var clusters []struct {
Projects []string `json:"projects"`
}
if err := json.NewDecoder(resp.Body).Decode(&clusters); err != nil {
return err
}

var listExistClusters []string = []string{}
var i int = 0
for _, cluster := range clusters {
for _, p := range cluster.Projects {
if strings.Contains(p, project.Name) {
i++
listExistClusters = append(listExistClusters, p)
}
}
}
if i == 1 {
return nil
}
return fmt.Errorf("project %s exists in other clusters: %v", project.Name, listExistClusters)
}

func deleteNamespace(namespaceName string) error {
kconfig, _ := rest.InClusterConfig()
clientSet, err := kubernetes.NewForConfig(kconfig)
if err != nil {
return err
}

return clientSet.CoreV1().Namespaces().Delete(context.TODO(), namespaceName, metav1.DeleteOptions{})
}

func deleteAppServiceAccount(namespaceName string) error {
kconfig, _ := rest.InClusterConfig()
clientSet, err := kubernetes.NewForConfig(kconfig)
if err != nil {
return err
}

return clientSet.CoreV1().ServiceAccounts(namespaceName).Delete(context.TODO(), utils.KubiServiceAccountAppName, metav1.DeleteOptions{})
}

func deleteNetworkPolicy(namespaceName string) error {
kconfig, _ := rest.InClusterConfig()
clientSet, err := kubernetes.NewForConfig(kconfig)
if err != nil {
return err
}

return clientSet.NetworkingV1().NetworkPolicies(namespaceName).Delete(context.TODO(), utils.KubiDefaultNetworkPolicyName, metav1.DeleteOptions{})
}

func deleteRoleBindings(namespaceName string) error {
kconfig, _ := rest.InClusterConfig()
clientSet, err := kubernetes.NewForConfig(kconfig)
if err != nil {
return err
}

// Delete all role bindings in the namespace that were created by Kubi
return clientSet.RbacV1().RoleBindings(namespaceName).DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{
LabelSelector: "creator=kubi",
})
}

// checkPodExistsInNamespace returns an error if there are any pods in the given namespace.
func checkPodExistsInNamespace(namespace string) error {
kconfig, err := rest.InClusterConfig()
if err != nil {
return err
}
clientSet, err := kubernetes.NewForConfig(kconfig)
if err != nil {
return err
}
pods, err := clientSet.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
if len(pods.Items) > 0 {
return fmt.Errorf("there are still %d pods in namespace %s", len(pods.Items), namespace)
}
return nil
}
3 changes: 2 additions & 1 deletion internal/utils/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ func MakeConfig() (*types.Config, error) {
PodSecurityAdmissionWarning: podSecurityAdmissionWarning,
PodSecurityAdmissionAudit: podSecurityAdmissionAudit,
Ldap: types.LdapConfig{
UserBase: ldapUserBase,
UserBase: ldapUserBase,
EligibleGroupsParents: ldapEligibleGroupsParents,
GroupBase: ldapGroupBase,
AppMasterGroupBase: getEnv("LDAP_APP_GROUPBASE", ""),
Expand Down Expand Up @@ -234,6 +234,7 @@ func MakeConfig() (*types.Config, error) {
Blacklist: strings.Split(getEnv("BLACKLIST", ""), ","),
Whitelist: whitelist,
BlackWhitelistNamespace: getEnv("BLACK_WHITELIST_NAMESPACE", "default"),
KgbApiURL: getEnv("KGB_API_URL", "https://kgb-api.devops.caas.cagip.group.gca"),
}, nil
}

Expand Down
1 change: 1 addition & 0 deletions pkg/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type Config struct {
Blacklist []string
BlackWhitelistNamespace string
Whitelist bool
KgbApiURL string
}

// Note: struct fields must be public in order for unmarshal to
Expand Down
1 change: 1 addition & 0 deletions test/e2e/conf/kubi/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ data:
PUBLIC_APISERVER_URL: https://kubernetes.default.svc.cluster.local
TENANT: cagip
TOKEN_LIFETIME: 4h
KGB_API_URL: http://localhost:9999
kind: ConfigMap
metadata:
labels:
Expand Down
Loading