diff --git a/cmd/doctor.go b/cmd/doctor.go index 1649eb8..096773c 100644 --- a/cmd/doctor.go +++ b/cmd/doctor.go @@ -7,6 +7,8 @@ import ( "github.com/spf13/cobra" ) +var imageTag string + var dockerCmd = &cobra.Command{ Use: "docker", Short: "Docker related helpers", @@ -16,8 +18,7 @@ var dockerInitCmd = &cobra.Command{ Use: "init", Short: "Generate a Dockerfile", Run: func(cmd *cobra.Command, args []string) { - err := docker.InitDockerfile() - if err != nil { + if err := docker.InitDockerfile(); err != nil { fmt.Println("ℹ️", err.Error()) return } @@ -25,7 +26,38 @@ var dockerInitCmd = &cobra.Command{ }, } +var dockerValidateCmd = &cobra.Command{ + Use: "validate", + Short: "Validate Dockerfile best practices", + Run: func(cmd *cobra.Command, args []string) { + if err := docker.ValidateDockerfile(); err != nil { + fmt.Println("❌", err.Error()) + } + }, +} + +var dockerBuildCmd = &cobra.Command{ + Use: "build", + Short: "Build Docker image", + Run: func(cmd *cobra.Command, args []string) { + if err := docker.BuildDockerImage(imageTag); err != nil { + fmt.Println("❌ Docker build failed") + } + }, +} + func init() { + dockerBuildCmd.Flags().StringVarP( + &imageTag, + "tag", + "t", + "", + "Docker image tag (default: codewise:latest)", + ) + dockerCmd.AddCommand(dockerInitCmd) + dockerCmd.AddCommand(dockerValidateCmd) + dockerCmd.AddCommand(dockerBuildCmd) + rootCmd.AddCommand(dockerCmd) } diff --git a/pkg/docker/docker.go b/pkg/docker/docker.go index 5eb9b72..9538b4e 100644 --- a/pkg/docker/docker.go +++ b/pkg/docker/docker.go @@ -1,20 +1,26 @@ package docker import ( + "bufio" "fmt" "os" + "os/exec" + "strings" ) const dockerfileName = "Dockerfile" -var defaultDockerfile = []byte(` -FROM golang:1.21-alpine AS builder +// InitDockerfile creates a Dockerfile if it doesn't exist +func InitDockerfile() error { + if _, err := os.Stat(dockerfileName); err == nil { + return fmt.Errorf("Dockerfile already exists") + } + defaultDockerfile := []byte(` +FROM golang:1.21-alpine AS builder WORKDIR /app - COPY go.mod go.sum ./ RUN go mod download - COPY . . RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app @@ -24,12 +30,68 @@ COPY --from=builder /app/app . EXPOSE 8080 CMD ["./app"] `) + return os.WriteFile(dockerfileName, defaultDockerfile, 0644) +} -// InitDockerfile creates a Dockerfile if it doesn't exist -func InitDockerfile() error { - if _, err := os.Stat(dockerfileName); err == nil { - return fmt.Errorf("Dockerfile already exists") +// ValidateDockerfile inspects Dockerfile best practices +func ValidateDockerfile() error { + file, err := os.Open(dockerfileName) + if err != nil { + return fmt.Errorf("Dockerfile not found") } + defer file.Close() - return os.WriteFile(dockerfileName, defaultDockerfile, 0644) + scanner := bufio.NewScanner(file) + + hasMultiStage := false + hasNonRoot := false + baseImage := "" + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + if strings.HasPrefix(line, "FROM") { + if baseImage == "" { + baseImage = line + } else { + hasMultiStage = true + } + } + + if strings.HasPrefix(line, "USER") { + hasNonRoot = true + } + } + + fmt.Println("Dockerfile validation:") + fmt.Println("----------------------") + fmt.Println("Base image:", baseImage) + + if hasMultiStage { + fmt.Println("✔ Multi-stage build detected") + } else { + fmt.Println("⚠ Single-stage build") + } + + if hasNonRoot { + fmt.Println("✔ Non-root user configured") + } else { + fmt.Println("⚠ Running as root user") + } + + return nil +} + +// BuildDockerImage runs docker build +func BuildDockerImage(tag string) error { + if tag == "" { + tag = "codewise:latest" + } + + cmd := exec.Command("docker", "build", "-t", tag, ".") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + fmt.Println("Running:", strings.Join(cmd.Args, " ")) + return cmd.Run() }