Skip to content

Commit

Permalink
Initial implementation of K8s-Deploy-Watcher with unit tests and GitH…
Browse files Browse the repository at this point in the history
…ub Actions

- Implemented K8s-Deploy-Watcher using Kubernetes Operator SDK.
- Added CRD definition and Reconciler logic to track Deployment status.
- Included unit tests using controller-runtime's fake client for local testing without Kubernetes.
- Set up GitHub Actions workflow for automated unit testing.
- Created Dockerfile for building and running the operator in a containerized environment.

Signed-off-by: ddukbg <[email protected]>
  • Loading branch information
ddukbg committed Oct 23, 2024
1 parent 0ee956a commit fdb96ca
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 0 deletions.
28 changes: 28 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Go CI

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Check out the repository
uses: actions/checkout@v2

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '1.16'

- name: Install dependencies
run: go mod download

- name: Run tests
run: go test ./... -v
14 changes: 14 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Dockerfile
FROM golang:1.16 as builder
WORKDIR /workspace
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o manager main.go

FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532
ENTRYPOINT ["/manager"]
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Makefile

# 빌드
build:
docker build -t k8s-deploy-watcher .

# 유닛 테스트 실행
test:
go test ./controllers/... -v
47 changes: 47 additions & 0 deletions api/v1alpha1/deployment_tracker_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// DeploymentTrackerSpec defines the desired state of DeploymentTracker
type DeploymentTrackerSpec struct {
DeploymentName string `json:"deploymentName,omitempty"`
Namespace string `json:"namespace,omitempty"`
Notify Notify `json:"notify"`
}

// Notify defines notification details (Slack or Email)
type Notify struct {
Slack string `json:"slack,omitempty"`
Email string `json:"email,omitempty"`
}

// DeploymentTrackerStatus defines the observed state of DeploymentTracker
type DeploymentTrackerStatus struct {
Ready bool `json:"ready,omitempty"`
}

// +kubebuilder:object:root=true

// DeploymentTracker is the Schema for the deploymenttrackers API
type DeploymentTracker struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec DeploymentTrackerSpec `json:"spec,omitempty"`
Status DeploymentTrackerStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// DeploymentTrackerList contains a list of DeploymentTracker
type DeploymentTrackerList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []DeploymentTracker `json:"items"`
}

func init() {
SchemeBuilder.Register(&DeploymentTracker{}, &DeploymentTrackerList{})
}
15 changes: 15 additions & 0 deletions api/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package v1alpha1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)

// GroupVersion is group version used to register these objects
var GroupVersion = scheme.GroupVersion{
Group: "ddukbg",
Version: "v1alpha1",
}

// SchemeBuilder adds the scheme to the manager
var SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
32 changes: 32 additions & 0 deletions config/crd/deployment_tracker.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: deploymenttrackers.ddukbg
spec:
group: ddukbg
names:
kind: DeploymentTracker
plural: deploymenttrackers
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
deploymentName:
type: string
namespace:
type: string
notify:
type: object
properties:
slack:
type: string
email:
type: string
10 changes: 10 additions & 0 deletions config/samples/deployment_tracker_my_app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
apiVersion: ddukbg/v1alpha1
kind: DeploymentTracker
metadata:
name: my-app-deployment-tracker
spec:
deploymentName: my-app
namespace: default
notify:
slack: "https://hooks.slack.com/services/..."
email: "[email protected]"
71 changes: 71 additions & 0 deletions controllers/deployment_tracker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package controllers

import (
"context"
"fmt"
"net/http"
"bytes"
"encoding/json"

appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

v1alpha1 "k8s-deploy-watcher/api/v1alpha1"
)

type DeploymentTrackerReconciler struct {
client.Client
Scheme *runtime.Scheme
}

func (r *DeploymentTrackerReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
logger := log.FromContext(ctx)

// 1. Custom Resource 가져오기
tracker := &v1alpha1.DeploymentTracker{}
err := r.Get(ctx, req.NamespacedName, tracker)
if err != nil {
logger.Error(err, "Failed to fetch DeploymentTracker CR")
return reconcile.Result{}, client.IgnoreNotFound(err)
}

// 2. 해당 Deployment의 상태 확인
deployment := &appsv1.Deployment{}
err = r.Get(ctx, client.ObjectKey{Name: tracker.Spec.DeploymentName, Namespace: tracker.Spec.Namespace}, deployment)
if err != nil {
logger.Error(err, "Failed to fetch Deployment")
return reconcile.Result{}, err
}

// 3. 배포가 성공적으로 완료되었는지 확인
if deployment.Status.ReadyReplicas == *deployment.Spec.Replicas {
logger.Info("Deployment is running successfully", "Deployment", deployment.Name)

// 4. 알림 전송
if tracker.Spec.Notify.Slack != "" {
err := sendSlackNotification(tracker.Spec.Notify.Slack, fmt.Sprintf("Deployment %s is successfully running", deployment.Name))
if err != nil {
logger.Error(err, "Failed to send Slack notification")
}
}
}

return reconcile.Result{}, nil
}

func sendSlackNotification(webhookURL, message string) error {
payload := map[string]string{"text": message}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return err
}

_, err = http.Post(webhookURL, "application/json", bytes.NewBuffer(payloadBytes))
if err != nil {
return err
}
return nil
}
59 changes: 59 additions & 0 deletions controllers/deployment_tracker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package controllers

import (
"context"
"testing"

appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/stretchr/testify/assert"
)

func TestReconcile_Success(t *testing.T) {
// 1. 가짜 클라이언트 생성 (fake.Client)
scheme := runtime.NewScheme()
_ = appsv1.AddToScheme(scheme) // 필요한 스키마 추가

// 테스트용 Deployment 생성
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "my-app",
Namespace: "default",
},
Spec: appsv1.DeploymentSpec{
Replicas: int32Ptr(2), // 2개의 파드
},
Status: appsv1.DeploymentStatus{
ReadyReplicas: 2,
},
}

// 가짜 클라이언트에 Deployment 추가
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(deployment).Build()

// 2. Reconciler 생성
r := &DeploymentTrackerReconciler{
Client: client,
Scheme: scheme,
}

// 3. Reconcile 호출
result, err := r.Reconcile(context.TODO(), reconcile.Request{
NamespacedName: types.NamespacedName{
Name: "my-app",
Namespace: "default",
},
})

// 4. 테스트 결과 확인
assert.NoError(t, err)
assert.NotNil(t, result)
assert.False(t, result.Requeue) // Requeue가 False여야 함 (정상 배포)
}

// 헬퍼 함수
func int32Ptr(i int32) *int32 { return &i }
32 changes: 32 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package main

import (
"os"
"sigs.k8s.io/controller-runtime/pkg/manager"
ctrl "sigs.k8s.io/controller-runtime"
"k8s-deploy-watcher/controllers"
"k8s-deploy-watcher/api/v1alpha1"
)

func main() {
// Manager 설정
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: v1alpha1.SchemeBuilder.AddToScheme,
})
if err != nil {
os.Exit(1)
}

// Reconciler 등록
if err := (&controllers.DeploymentTrackerReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
os.Exit(1)
}

// Operator 실행
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
os.Exit(1)
}
}

0 comments on commit fdb96ca

Please sign in to comment.