0

Pre-flight Checks Bài 1: Khởi tạo CLI Engine & Kiến trúc Pre-flight Runner

Trong một thư viện hay công cụ chuyên nghiệp, nếu viết nối tiếp các câu lệnh if-else để kiểm tra từng điều kiện, code sẽ rất nhanh trở thành một mớ hỗn độn (spaghetti code) khi số lượng bài kiểm tra tăng lên 10–15 bước.

Giải pháp chuẩn mực của các thư viện lớn là áp dụng Interface & Pipeline Pattern: Mỗi bài kiểm tra (Check) là một module độc lập thực thi chung một Interface, và có một bộ Runner đứng ra điều phối, thu thập kết quả và dừng quy trình ngay lập tức nếu có một mắt xích báo lỗi (Fail-fast).

1. Thiết kế cấu trúc thư mục

Tạo một thư mục dự án mới tên là godeploy với cấu trúc tách biệt rõ ràng giữa lõi xử lý (internal/preflight) và điểm chạy chương trình (cmd):

Plaintext

godeploy/
├── cmd/
│   └── main.go                 # Điểm khởi chạy CLI
├── internal/
│   └── preflight/
│       ├── check.go            # Định nghĩa Interface & Struct kết quả
│       ├── runner.go           # Bộ điều phối chạy các bài kiểm tra
│       └── dummy_check.go      # Bài kiểm tra mẫu để test khung
└── go.mod

Khởi tạo Go module trong thư mục godeploy:

Bash

mkdir -p godeploy/cmd godeploy/internal/preflight
cd godeploy
go mod init godeploy

2. Định nghĩa Hợp đồng chung (internal/preflight/check.go)

Mỗi bước kiểm tra trước giờ G cần trả về 3 trạng thái rõ ràng: Pass (Đạt), Warn (Cảnh báo nhưng vẫn cho đi tiếp), hoặc Fail (Lỗi nghiêm trọng - Hủy deploy ngay).

Tạo file internal/preflight/check.go:

Go

package preflight

import (
	"context"
	"time"
)

// Status đại diện cho trạng thái của một bài kiểm tra
type Status int

const (
	StatusPass Status = iota // 0: Thành công
	StatusWarn               // 1: Cảnh báo (không chặn deploy)
	StatusFail               // 2: Thất bại (chặn đứng deploy)
)

// Result lưu trữ thông tin chi tiết sau khi một Check chạy xong
type Result struct {
	Name     string        // Tên bài kiểm tra
	Status   Status        // Trạng thái đạt/lỗi
	Message  string        // Thông báo chi tiết cho Dev hiểu chuyện gì đang xảy ra
	FixHint  string        // Gợi ý lệnh hoặc cách sửa nhanh cho Dev
	Duration time.Duration // Thời gian thực thi bước kiểm tra này
}

// Check là Interface chuẩn mà mọi bài kiểm tra (Git, Disk, Sonar, Lock...) phải tuân theo
type Check interface {
	Name() string
	Run(ctx context.Context) Result
}

3. Xây dựng Bộ điều phối (internal/preflight/runner.go)

Bộ Runner sẽ nhận vào một danh sách các Check, chạy lần lượt từng bước, đo thời gian thực thi và in kết quả trực quan ra màn hình Terminal kèm màu sắc ANSI.

Tạo file internal/preflight/runner.go:

Go

package preflight

import (
	"context"
	"fmt"
	"time"
)

// Mã màu ANSI giúp Terminal hiển thị trực quan mà chưa cần cài thư viện ngoài
const (
	colorReset  = "\033[0m"
	colorRed    = "\033[31m"
	colorGreen  = "\033[32m"
	colorYellow = "\033[33m"
	colorCyan   = "\033[36m"
	colorBold   = "\033[1m"
)

type Runner struct {
	checks   []Check
	failFast bool // Nếu true: Dừng ngay lập tức khi gặp lỗi đầu tiên
}

func NewRunner(failFast bool, checks ...Check) *Runner {
	return &Runner{
		checks:   checks,
		failFast: failFast,
	}
}

// Execute chạy toàn bộ danh sách kiểm tra và trả về true nếu được phép deploy
func (r *Runner) Execute(ctx context.Context) bool {
	fmt.Printf("\n%s%s🚀 BẮT ĐẦU PRE-FLIGHT CHECKS (KIỂM TRA TRƯỚC GIỜ G)...%s\n", colorBold, colorCyan, colorReset)
	fmt.Println("------------------------------------------------------------")

	allPassed := true
	startTime := time.Now()

	for i, check := range r.checks {
		stepStart := time.Now()
		fmt.Printf("[%d/%d] Đang kiểm tra: %-30s ", i+1, len(r.checks), check.Name()+"...")

		// Thực thi bài kiểm tra
		res := check.Run(ctx)
		res.Name = check.Name()
		res.Duration = time.Since(stepStart)

		// Hiển thị kết quả ngay trên cùng dòng
		switch res.Status {
		case StatusPass:
			fmt.Printf("%s[PASS]%s (%dms)\n", colorGreen, colorReset, res.Duration.Milliseconds())
			if res.Message != "" {
				fmt.Printf("      └─ %s\n", res.Message)
			}
		case StatusWarn:
			fmt.Printf("%s[WARN]%s (%dms)\n", colorYellow, colorReset, res.Duration.Milliseconds())
			fmt.Printf("      └─ ⚠️  %s\n", res.Message)
		case StatusFail:
			allPassed = false
			fmt.Printf("%s[FAIL]%s (%dms)\n", colorRed, colorReset, res.Duration.Milliseconds())
			fmt.Printf("      ├─ ❌ Lỗi: %s%s%s\n", colorRed, res.Message, colorReset)
			if res.FixHint != "" {
				fmt.Printf("      └─ 💡 Cách xử lý: %s%s%s\n", colorYellow, res.FixHint, colorReset)
			}
		}

		// Nếu bật chế độ Fail-Fast và gặp lỗi nghiêm trọng thì ngắt luôn
		if res.Status == StatusFail && r.failFast {
			fmt.Println("------------------------------------------------------------")
			fmt.Printf("%s%s⛔ QUY TRÌNH DEPLOY BỊ CHẶN ĐỨNG DO LỖI PRE-FLIGHT!%s\n\n", colorBold, colorRed, colorReset)
			return false
		}
	}

	fmt.Println("------------------------------------------------------------")
	totalTime := time.Since(startTime).Milliseconds()

	if allPassed {
		fmt.Printf("%s%s✅ TẤT CẢ KIỂM TRA HOÀN TẤT (%dms) - SẴN SÀNG DEPLOY!%s\n\n", colorBold, colorGreen, totalTime, colorReset)
		return true
	}

	fmt.Printf("%s%s⛔ PHÁT HIỆN LỖI (%dms) - VUI LÒNG KHẮC PHỤC TRƯỚC KHI DEPLOY!%s\n\n", colorBold, colorRed, totalTime, colorReset)
	return false
}

4. Viết một bài kiểm tra mẫu để test khung (internal/preflight/dummy_check.go)

Trước khi viết các bài kiểm tra thật ở các bài sau, ta tạo một Check kiểm tra kết nối mạng cơ bản và phiên bản môi trường để xem Runner vận hành ra sao.

Tạo file internal/preflight/dummy_check.go:

Go

package preflight

import (
	"context"
	"os"
	"time"
)

// EnvFileCheck kiểm tra xem dự án hiện tại có file cấu hình .deploy.yaml chưa
type EnvFileCheck struct {
	ConfigPath string
}

func (c *EnvFileCheck) Name() string {
	return "Kiểm tra file cấu hình Deploy"
}

func (c *EnvFileCheck) Run(ctx context.Context) Result {
	// Giả lập độ trễ 100ms cho giống cảm giác chạy thực tế
	time.Sleep(100 * time.Millisecond)

	if _, err := os.Stat(c.ConfigPath); os.IsNotExist(err) {
		return Result{
			Status:  StatusFail,
			Message: "Không tìm thấy file cấu hình " + c.ConfigPath + " tại thư mục gốc dự án.",
			FixHint: "Chạy lệnh `godeploy init` hoặc tạo file " + c.ConfigPath + " theo mẫu của team.",
		}
	}

	return Result{
		Status:  StatusPass,
		Message: "Đã tìm thấy file " + c.ConfigPath + " hợp lệ.",
	}
}

5. Điểm khởi chạy chương trình (cmd/main.go)

Tạo file cmd/main.go để ghép nối Runner và bài kiểm tra:

Go

package main

import (
	"context"
	"godeploy/internal/preflight"
	"os"
	"time"
)

func main() {
	// Đặt timeout tổng cho toàn bộ quá trình Pre-flight là 30 giây
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Khởi tạo danh sách các bài kiểm tra
	checks := []preflight.Check{
		&preflight.EnvFileCheck{ConfigPath: ".deploy.yaml"},
	}

	// Khởi tạo Runner với chế độ failFast = true (gặp lỗi là dừng ngay)
	runner := preflight.NewRunner(true, checks...)

	// Nếu Pre-flight thất bại, trả về mã lỗi Exit Code 1 (rất quan trọng nếu tích hợp vào CI/CD)
	if !runner.Execute(ctx) {
		os.Exit(1)
	}

	// Nếu vượt qua toàn bộ Pre-flight, bước Deploy chính thức mới được gọi ở dưới đây
}

6. Chạy thử nghiệm (Thực hành)

Trường hợp 1: Chưa có file .deploy.yaml (Bị chặn đứng) Chạy lệnh sau tại thư mục godeploy:

Bash

go run cmd/main.go

Kết quả trên Terminal của bạn sẽ hiển thị sắc nét:

Plaintext

🚀 BẮT ĐẦU PRE-FLIGHT CHECKS (KIỂM TRA TRƯỚC GIỜ G)...
------------------------------------------------------------
[1/1] Đang kiểm tra: Kiểm tra file cấu hình Deploy... [FAIL] (101ms)
      ├─ ❌ Lỗi: Không tìm thấy file cấu hình .deploy.yaml tại thư mục gốc dự án.
      └─ 💡 Cách xử lý: Chạy lệnh `godeploy init` hoặc tạo file .deploy.yaml theo mẫu của team.
------------------------------------------------------------
⛔ QUY TRÌNH DEPLOY BỊ CHẶN ĐỨNG DO LỖI PRE-FLIGHT!
exit status 1

Trường hợp 2: Tạo file .deploy.yaml và chạy lại (Vượt qua)

Bash

touch .deploy.yaml
go run cmd/main.go

Kết quả:

Plaintext

🚀 BẮT ĐẦU PRE-FLIGHT CHECKS (KIỂM TRA TRƯỚC GIỜ G)...
------------------------------------------------------------
[1/1] Đang kiểm tra: Kiểm tra file cấu hình Deploy... [PASS] (100ms)
      └─ Đã tìm thấy file .deploy.yaml hợp lệ.
------------------------------------------------------------
✅ TẤT CẢ KIỂM TRA HOÀN TẤT (100ms) - SẴN SÀNG DEPLOY!

Nhờ kiến trúc này, từ Bài 2 trở đi, mỗi khi muốn thêm một chốt chặn mới (Git, SonarQube, SSH Disk Check, Redis Lock...), bạn chỉ cần viết 1 file rời có hàm Run(ctx) và nhét vào mảng checks trong main.go mà không phải sửa lại bộ khung hiển thị hay điều phối nữa.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí