Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: application set handler #241

Merged
merged 16 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .mockery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ packages:
# place your package-specific config here
config:
all: true
github.com/zapier/kubechecks/pkg/generator:
# place your package-specific config here
config:
all: true
github.com/zapier/kubechecks/pkg/affected_apps:
# place your package-specific config here
config:
all: true
13 changes: 6 additions & 7 deletions Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ load('ext://tests/golang', 'test_go')
load('ext://namespace', 'namespace_create')
load('ext://uibutton', 'cmd_button')
load('ext://helm_resource', 'helm_resource')
load('ext://local_output', 'local_output')
load('./.tilt/terraform/Tiltfile', 'local_terraform_resource')
load('./.tilt/utils/Tiltfile', 'check_env_set')

Expand Down Expand Up @@ -144,10 +145,6 @@ test_go(
)


# get the git commit ref
def get_git_head():
result = local('git rev-parse --short HEAD')
return result

# read .tool-versions file and return a dictionary of tools and their versions
def parse_tool_versions(fn):
Expand All @@ -174,7 +171,9 @@ def parse_tool_versions(fn):
return tools

tool_versions = parse_tool_versions(".tool-versions")
git_commit = str(get_git_head()).strip()

# get the git commit ref
git_commit = local_output('git rev-parse --short HEAD')

earthly_build(
context='.',
Expand Down Expand Up @@ -260,8 +259,8 @@ k8s_resource(
load("localdev/test_apps/Tiltfile", "install_test_apps")
install_test_apps(cfg)

load("localdev/test_appsets/Tiltfile", "install_test_appsets")
install_test_appsets(cfg)
load("localdev/test_appsets/Tiltfile", "copy_test_appsets")
copy_test_appsets(cfg)


force_argocd_cleanup_on_tilt_down()
7 changes: 7 additions & 0 deletions charts/kubechecks-rbac/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v2
name: kubechecks-rbac
description: A Helm chart for kubechecks Role and RoleBinding
version: 0.4.5
type: application
maintainers:
- name: zapier
11 changes: 11 additions & 0 deletions charts/kubechecks-rbac/templates/role.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ .Values.roleName | default "kubechecks-remote-role" }}
djeebus marked this conversation as resolved.
Show resolved Hide resolved
rules:
- apiGroups: ['argoproj.io']
resources: ['applications', 'appprojects', 'applicationsets', 'services']
verbs: ['get', 'list', 'watch']
- apiGroups: [''] # The core API group, which is indicated by an empty string
resources: ['secrets']
verbs: ['get', 'list', 'watch']
21 changes: 21 additions & 0 deletions charts/kubechecks-rbac/templates/rolebinding.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ .Values.roleBindingName | default "kubechecks-remote-role-binding" }}
namespace: {{ .Values.namespace | default "argocd" }}
subjects:
{{- if .Values.eks.iamRoleArn }}
- kind: User
name: {{ .Values.eks.iamRoleArn }}
apiGroup: rbac.authorization.k8s.io
{{- else if .Values.selfManaged.serviceAccountName }}
- kind: ServiceAccount
name: {{ .Values.selfManaged.serviceAccountName }}
namespace: {{ .Values.namespace | default "argocd" }}
{{- else }}
{{ fail "Either eks.iamRoleArn, or selfManaged.serviceAccountName must be provided" }}
{{- end }}
roleRef:
kind: Role
name: {{ .Values.roleName | default "kubechecks-remote-role" }}
apiGroup: rbac.authorization.k8s.io
15 changes: 15 additions & 0 deletions charts/kubechecks-rbac/tests/role_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
suite: role tests

templates:
- role.yaml

tests:
- it: should create a Role with the correct name
set:
roleName: "kubechecks-test-role"
asserts:
- isKind:
of: Role
- equal:
path: metadata.name
value: kubechecks-test-role
29 changes: 29 additions & 0 deletions charts/kubechecks-rbac/tests/rolebinding_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
suite: role binding tests

templates:
- rolebinding.yaml

tests:
- it: should create a RoleBinding with the correct name with EKS IAM role
set:
roleBindingName: "kubechecks-test-rolebinding-rbac"
eks:
iamRoleArn: "arn:aws:iam::123456789012:role/example-role"
asserts:
- isKind:
of: RoleBinding
- equal:
path: metadata.name
value: kubechecks-test-rolebinding-rbac

- it: should create a RoleBinding with the correct name with self-managed service account
set:
roleBindingName: "kubechecks-test-rolebinding-rbac2"
selfManaged:
serviceAccountName: "kubecheck-sa"
asserts:
- isKind:
of: RoleBinding
- equal:
path: metadata.name
value: kubechecks-test-rolebinding-rbac2
48 changes: 48 additions & 0 deletions charts/kubechecks-rbac/values.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Kubechecks Values Schema",
"type": "object",
"properties": {
"roleName": {
"type": "string",
"description": "The name of the Role to be created.",
"default": "kubechecks-remote-role"
},
"roleBindingName": {
"type": "string",
"description": "The name of the RoleBinding to be created.",
"default": "kubechecks-remote-role-binding"
},
"namespace": {
"type": "string",
"description": "The namespace where the Role and RoleBinding will be created.",
"default": "argocd"
},
"eks": {
"type": "object",
"description": "Configuration for AWS EKS clusters.",
"properties": {
"iamRoleArn": {
"type": "string",
"description": "The ARN of the IAM role to bind to the Role."
}
},
"required": ["iamRoleArn"],
"additionalProperties": false
},
"selfManaged": {
"type": "object",
"description": "Configuration for self-managed clusters.",
"properties": {
"serviceAccountName": {
"type": "string",
"description": "The name of the ServiceAccount to bind to the Role."
}
},
"required": ["serviceAccountName"],
"additionalProperties": false
}
},
"required": ["roleName", "roleBindingName", "namespace"],
"additionalProperties": false
}
13 changes: 13 additions & 0 deletions charts/kubechecks-rbac/values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
roleName: "kubechecks-remote-role"
roleBindingName: "kubechecks-remote-role-binding"
namespace: "argocd"

# for AWS EKS clusters
eks:
# The ARN of the IAM role to bind to the Role
iamRoleArn: ""

# for self-managed clusters
selfManaged:
# The name of the ServiceAccount to bind to the Role
serviceAccountName: ""
2 changes: 1 addition & 1 deletion charts/kubechecks/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: v2
name: kubechecks
description: A Helm chart for kubechecks
version: 0.4.4
version: 0.4.5
type: application
maintainers:
- name: zapier
5 changes: 4 additions & 1 deletion charts/kubechecks/templates/clusterrole.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ metadata:
name: {{ include "kubechecks.fullname" . }}
rules:
- apiGroups: ['argoproj.io']
resources: ['applications', 'appprojects', 'services']
resources: ['applications', 'appprojects', 'applicationsets', 'services']
verbs: ['get', 'list', 'watch']
- apiGroups: [''] # The core API group, which is indicated by an empty string
resources: ['secrets']
verbs: ['get', 'list', 'watch']
1 change: 1 addition & 0 deletions charts/kubechecks/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ configMap:
env: {}
# KUBECHECKS_ARGOCD_API_INSECURE: "false"
# KUBECHECKS_ARGOCD_API_PATH_PREFIX: /
# KUBECHECKS_ARGOCD_API_NAMESPACE: argocd
# KUBECHECKS_ARGOCD_WEBHOOK_URL: https://argocd.<domain.com>/api/webhook
# KUBECHECKS_FALLBACK_K8S_VERSION: "1.22.0"
# KUBECHECKS_LOG_LEVEL: debug
Expand Down
45 changes: 44 additions & 1 deletion cmd/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/zapier/kubechecks/pkg/config"
"github.com/zapier/kubechecks/pkg/container"
"github.com/zapier/kubechecks/pkg/git"
client "github.com/zapier/kubechecks/pkg/kubernetes"
"github.com/zapier/kubechecks/pkg/vcs/github_client"
"github.com/zapier/kubechecks/pkg/vcs/gitlab_client"
)
Expand All @@ -36,7 +37,30 @@ func newContainer(ctx context.Context, cfg config.ServerConfig, watchApps bool)
if err != nil {
return ctr, errors.Wrap(err, "failed to create vcs client")
}
var kubeClient client.Interface

switch cfg.KubernetesType {
// TODO: expand with other cluster types
case client.ClusterTypeLOCAL:
kubeClient, err = client.New(&client.NewClientInput{
KubernetesConfigPath: cfg.KubernetesConfig,
ClusterType: cfg.KubernetesType,
})
if err != nil {
return ctr, errors.Wrap(err, "failed to create kube client")
}
case client.ClusterTypeEKS:
Copy link
Collaborator

@MeNsaaH MeNsaaH Jul 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want to do this? Feels like it'll introduce a lot of complexities with managing cluster authentication 🤔. Should kubechecks watch resources in other clusters?

IMHO, kubechecks should be deployed to clusters where argocd is deployed so that it can monitor argocd. That means multiple argocd instances, multiple kubechecks

WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alternative is pretty gnarly. IAM roles, cross account trusts, etc, assuming it can actually be done. This seems relatively simple (compared to the rest of the features), and fairly isolated/repeatable.

kubeClient, err = client.New(&client.NewClientInput{
KubernetesConfigPath: cfg.KubernetesConfig,
ClusterType: cfg.KubernetesType,
},
client.EKSClientOption(ctx, cfg.KubernetesClusterID, cfg.KubernetesClusterRegion),
)
if err != nil {
return ctr, errors.Wrap(err, "failed to create kube client")
}
}
ctr.KubeClientSet = kubeClient
// create argo client
if ctr.ArgoClient, err = argo_client.NewArgoClient(cfg); err != nil {
return ctr, errors.Wrap(err, "failed to create argo client")
Expand All @@ -52,13 +76,22 @@ func newContainer(ctx context.Context, cfg config.ServerConfig, watchApps bool)
return ctr, errors.Wrap(err, "failed to build apps map")
}

if err = buildAppSetsMap(ctx, ctr.ArgoClient, ctr.VcsToArgoMap); err != nil {
return ctr, errors.Wrap(err, "failed to build appsets map")
}

if watchApps {
ctr.ApplicationWatcher, err = app_watcher.NewApplicationWatcher(vcsToArgoMap, cfg)
ctr.ApplicationWatcher, err = app_watcher.NewApplicationWatcher(kubeClient.Config(), vcsToArgoMap, cfg)
if err != nil {
return ctr, errors.Wrap(err, "failed to create watch applications")
}
ctr.ApplicationSetWatcher, err = app_watcher.NewApplicationSetWatcher(kubeClient.Config(), vcsToArgoMap, cfg)
if err != nil {
return ctr, errors.Wrap(err, "failed to create watch application sets")
}

go ctr.ApplicationWatcher.Run(ctx, 1)
go ctr.ApplicationSetWatcher.Run(ctx)
}
} else {
log.Info().Msgf("not monitoring applications, MonitorAllApplications: %+v", cfg.MonitorAllApplications)
Expand All @@ -75,6 +108,16 @@ func buildAppsMap(ctx context.Context, argoClient *argo_client.ArgoClient, resul
for _, app := range apps.Items {
result.AddApp(&app)
}
return nil
}

func buildAppSetsMap(ctx context.Context, argoClient *argo_client.ArgoClient, result container.VcsToArgoMap) error {
appSets, err := argoClient.GetApplicationSets(ctx)
if err != nil {
return errors.Wrap(err, "failed to list application sets")
}
for _, appSet := range appSets.Items {
result.AddAppSet(&appSet)
}
return nil
}
11 changes: 7 additions & 4 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ func init() {
stringFlag(flags, "argocd-api-namespace", "ArgoCD namespace where the application watcher will read Custom Resource Definitions (CRD) for Application and ApplicationSet resources.",
newStringOpts().
withDefault("argocd"))
stringFlag(flags, "kubernetes-type", "Kubernetes Type One of eks, or local. Defaults to local.",
newStringOpts().
withChoices("eks", "local").
withDefault("local"))
stringFlag(flags, "kubernetes-clusterid", "Kubernetes Cluster ID, must be specified if kubernetes-type is eks.")
stringFlag(flags, "kubernetes-cluster-region", "Kubernetes Cluster Region, if the cluster is on a cloud provider, and running on a different region, this must be specified.")
stringFlag(flags, "kubernetes-config", "Path to your kubernetes config file, used to monitor applications.")

stringFlag(flags, "otel-collector-port", "The OpenTelemetry collector port.")
Expand All @@ -73,10 +79,7 @@ func init() {
newStringOpts().
withChoices("hide", "delete").
withDefault("hide"))
stringSliceFlag(flags, "schemas-location", "Sets schema locations to be used for every check request. Can be common paths inside the repos being checked or git urls in either git or http(s) format.",
newStringSliceOpts().
withDefault([]string{"./schemas"}))

stringSliceFlag(flags, "schemas-location", "Sets schema locations to be used for every check request. Can be common paths inside the repos being checked or git urls in either git or http(s) format.")
boolFlag(flags, "enable-conftest", "Set to true to enable conftest policy checking of manifests.")
stringSliceFlag(flags, "policies-location", "Sets rego policy locations to be used for every check request. Can be common path inside the repos being checked or git urls in either git or http(s) format.",
newStringSliceOpts().
Expand Down
8 changes: 8 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,11 @@ By abstracting the PR/MR in this way, `kubechecks` remains VCS provider agnostic
![Check Event and Repo type diagrams](./img/checkevent.png){: style="height:350px;display:block;margin:0 auto;"}

The final piece of the puzzle is the `CheckEvent`; an internal structure that takes a `Client` and a `Repo` and begins running all configured checks. A `CheckEvent` first determines what applications within the repository have been affected by the PR/MR, and begins concurrently running the check suite against each affected application to generate a report for that app. As each application updates its report, the `CheckEvent` compiles all reports together and instructs the `Client` to update the PR/MR with a comment detailing the current progress; resulting in one comment per run of `kubechecks` with the latest information about that particular run. Whenever a new run of `kubechecks` is initiated, all previous comments are deleted to reduce clutter.


### Event Flows Diagram

![Event Flow Diagram](./img/eventflowdiagram.png){: style="height:350px;display:block;margin:0 auto;"}

This diagram illustrates the flow of events from the initial webhook trigger to the final report generation and comment update process.

Binary file added docs/img/eventflowdiagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ The full list of supported environment variables is described below:
|`KUBECHECKS_ENABLE_PREUPGRADE`|Enable preupgrade checks.|`true`|
|`KUBECHECKS_ENSURE_WEBHOOKS`|Ensure that webhooks are created in repositories referenced by argo.|`false`|
|`KUBECHECKS_FALLBACK_K8S_VERSION`|Fallback target Kubernetes version for schema / upgrade checks.|`1.23.0`|
|`KUBECHECKS_KUBERNETES_CLUSTER_REGION`|Kubernetes Cluster Region, if the cluster is on a cloud provider, and running on a different region, this must be specified.||
|`KUBECHECKS_KUBERNETES_CLUSTERID`|Kubernetes Cluster ID, must be specified if kubernetes-type is eks.||
|`KUBECHECKS_KUBERNETES_CONFIG`|Path to your kubernetes config file, used to monitor applications.||
|`KUBECHECKS_KUBERNETES_TYPE`|Kubernetes Type One of eks, or local.|`local`|
|`KUBECHECKS_LABEL_FILTER`|(Optional) If set, The label that must be set on an MR (as "kubechecks:<value>") for kubechecks to process the merge request webhook.||
|`KUBECHECKS_LOG_LEVEL`|Set the log output level. One of error, warn, info, debug, trace.|`info`|
|`KUBECHECKS_MAX_CONCURRENCT_CHECKS`|Number of concurrent checks to run.|`32`|
Expand All @@ -59,7 +62,7 @@ The full list of supported environment variables is described below:
|`KUBECHECKS_PERSIST_LOG_LEVEL`|Persists the set log level down to other module loggers.|`false`|
|`KUBECHECKS_POLICIES_LOCATION`|Sets rego policy locations to be used for every check request. Can be common path inside the repos being checked or git urls in either git or http(s) format.|`[./policies]`|
|`KUBECHECKS_REPO_REFRESH_INTERVAL`|Interval between static repo refreshes (for schemas and policies).|`5m`|
|`KUBECHECKS_SCHEMAS_LOCATION`|Sets schema locations to be used for every check request. Can be common paths inside the repos being checked or git urls in either git or http(s) format.|`[./schemas]`|
|`KUBECHECKS_SCHEMAS_LOCATION`|Sets schema locations to be used for every check request. Can be common paths inside the repos being checked or git urls in either git or http(s) format.|`[]`|
|`KUBECHECKS_SHOW_DEBUG_INFO`|Set to true to print debug info to the footer of MR comments.|`false`|
|`KUBECHECKS_TIDY_OUTDATED_COMMENTS_MODE`|Sets the mode to use when tidying outdated comments. One of hide, delete.|`hide`|
|`KUBECHECKS_VCS_BASE_URL`|VCS base url, useful if self hosting gitlab, enterprise github, etc.||
Expand Down
Loading
Loading