|
| 1 | +package reloader |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "os" |
| 7 | + "os/exec" |
| 8 | + "path/filepath" |
| 9 | + "strings" |
| 10 | + "time" |
| 11 | +) |
| 12 | + |
| 13 | +// Config holds configuration for a Reloader deployment. |
| 14 | +type Config struct { |
| 15 | + Version string |
| 16 | + Image string |
| 17 | + Namespace string |
| 18 | + ReloadStrategy string |
| 19 | +} |
| 20 | + |
| 21 | +// Manager handles Reloader deployment operations. |
| 22 | +type Manager struct { |
| 23 | + config Config |
| 24 | + kubeContext string |
| 25 | +} |
| 26 | + |
| 27 | +// NewManager creates a new Reloader manager. |
| 28 | +func NewManager(config Config) *Manager { |
| 29 | + return &Manager{ |
| 30 | + config: config, |
| 31 | + } |
| 32 | +} |
| 33 | + |
| 34 | +// SetKubeContext sets the kubeconfig context to use. |
| 35 | +func (m *Manager) SetKubeContext(kubeContext string) { |
| 36 | + m.kubeContext = kubeContext |
| 37 | +} |
| 38 | + |
| 39 | +// kubectl returns kubectl command with optional context. |
| 40 | +func (m *Manager) kubectl(ctx context.Context, args ...string) *exec.Cmd { |
| 41 | + if m.kubeContext != "" { |
| 42 | + args = append([]string{"--context", m.kubeContext}, args...) |
| 43 | + } |
| 44 | + return exec.CommandContext(ctx, "kubectl", args...) |
| 45 | +} |
| 46 | + |
| 47 | +// namespace returns the namespace for this reloader instance. |
| 48 | +func (m *Manager) namespace() string { |
| 49 | + if m.config.Namespace != "" { |
| 50 | + return m.config.Namespace |
| 51 | + } |
| 52 | + return fmt.Sprintf("reloader-%s", m.config.Version) |
| 53 | +} |
| 54 | + |
| 55 | +// releaseName returns the release name for this instance. |
| 56 | +func (m *Manager) releaseName() string { |
| 57 | + return fmt.Sprintf("reloader-%s", m.config.Version) |
| 58 | +} |
| 59 | + |
| 60 | +// Job returns the Prometheus job name for this Reloader instance. |
| 61 | +func (m *Manager) Job() string { |
| 62 | + return fmt.Sprintf("reloader-%s", m.config.Version) |
| 63 | +} |
| 64 | + |
| 65 | +// Deploy deploys Reloader to the cluster using raw manifests. |
| 66 | +func (m *Manager) Deploy(ctx context.Context) error { |
| 67 | + ns := m.namespace() |
| 68 | + name := m.releaseName() |
| 69 | + |
| 70 | + fmt.Printf("Deploying Reloader (%s) with image %s...\n", m.config.Version, m.config.Image) |
| 71 | + |
| 72 | + manifest := m.buildManifest(ns, name) |
| 73 | + |
| 74 | + applyCmd := m.kubectl(ctx, "apply", "-f", "-") |
| 75 | + applyCmd.Stdin = strings.NewReader(manifest) |
| 76 | + applyCmd.Stdout = os.Stdout |
| 77 | + applyCmd.Stderr = os.Stderr |
| 78 | + if err := applyCmd.Run(); err != nil { |
| 79 | + return fmt.Errorf("applying manifest: %w", err) |
| 80 | + } |
| 81 | + |
| 82 | + fmt.Printf("Waiting for Reloader deployment to be ready...\n") |
| 83 | + waitCmd := m.kubectl(ctx, "rollout", "status", "deployment", name, |
| 84 | + "-n", ns, |
| 85 | + "--timeout=120s") |
| 86 | + waitCmd.Stdout = os.Stdout |
| 87 | + waitCmd.Stderr = os.Stderr |
| 88 | + if err := waitCmd.Run(); err != nil { |
| 89 | + return fmt.Errorf("waiting for deployment: %w", err) |
| 90 | + } |
| 91 | + |
| 92 | + time.Sleep(2 * time.Second) |
| 93 | + |
| 94 | + fmt.Printf("Reloader (%s) deployed successfully\n", m.config.Version) |
| 95 | + return nil |
| 96 | +} |
| 97 | + |
| 98 | +// buildManifest creates the raw Kubernetes manifest for Reloader. |
| 99 | +func (m *Manager) buildManifest(ns, name string) string { |
| 100 | + var args []string |
| 101 | + args = append(args, "--log-format=json") |
| 102 | + if m.config.ReloadStrategy != "" && m.config.ReloadStrategy != "default" { |
| 103 | + args = append(args, fmt.Sprintf("--reload-strategy=%s", m.config.ReloadStrategy)) |
| 104 | + } |
| 105 | + |
| 106 | + argsYAML := "" |
| 107 | + if len(args) > 0 { |
| 108 | + argsYAML = " args:\n" |
| 109 | + for _, arg := range args { |
| 110 | + argsYAML += fmt.Sprintf(" - %q\n", arg) |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + return fmt.Sprintf(`--- |
| 115 | +apiVersion: v1 |
| 116 | +kind: Namespace |
| 117 | +metadata: |
| 118 | + name: %[1]s |
| 119 | +--- |
| 120 | +apiVersion: v1 |
| 121 | +kind: ServiceAccount |
| 122 | +metadata: |
| 123 | + name: %[2]s |
| 124 | + namespace: %[1]s |
| 125 | +--- |
| 126 | +apiVersion: rbac.authorization.k8s.io/v1 |
| 127 | +kind: ClusterRole |
| 128 | +metadata: |
| 129 | + name: %[2]s |
| 130 | +rules: |
| 131 | +- apiGroups: ["*"] |
| 132 | + resources: ["*"] |
| 133 | + verbs: ["*"] |
| 134 | +--- |
| 135 | +apiVersion: rbac.authorization.k8s.io/v1 |
| 136 | +kind: ClusterRoleBinding |
| 137 | +metadata: |
| 138 | + name: %[2]s |
| 139 | +roleRef: |
| 140 | + apiGroup: rbac.authorization.k8s.io |
| 141 | + kind: ClusterRole |
| 142 | + name: %[2]s |
| 143 | +subjects: |
| 144 | +- kind: ServiceAccount |
| 145 | + name: %[2]s |
| 146 | + namespace: %[1]s |
| 147 | +--- |
| 148 | +apiVersion: apps/v1 |
| 149 | +kind: Deployment |
| 150 | +metadata: |
| 151 | + name: %[2]s |
| 152 | + namespace: %[1]s |
| 153 | + labels: |
| 154 | + app: %[2]s |
| 155 | + app.kubernetes.io/name: reloader |
| 156 | + loadtest-version: %[3]s |
| 157 | +spec: |
| 158 | + replicas: 1 |
| 159 | + selector: |
| 160 | + matchLabels: |
| 161 | + app: %[2]s |
| 162 | + template: |
| 163 | + metadata: |
| 164 | + labels: |
| 165 | + app: %[2]s |
| 166 | + app.kubernetes.io/name: reloader |
| 167 | + loadtest-version: %[3]s |
| 168 | + annotations: |
| 169 | + prometheus.io/scrape: "true" |
| 170 | + prometheus.io/port: "9090" |
| 171 | + prometheus.io/path: "/metrics" |
| 172 | + spec: |
| 173 | + serviceAccountName: %[2]s |
| 174 | + securityContext: |
| 175 | + runAsNonRoot: true |
| 176 | + runAsUser: 65534 |
| 177 | + containers: |
| 178 | + - name: reloader |
| 179 | + image: %[4]s |
| 180 | + imagePullPolicy: IfNotPresent |
| 181 | + ports: |
| 182 | + - name: http |
| 183 | + containerPort: 9090 |
| 184 | +%[5]s resources: |
| 185 | + requests: |
| 186 | + cpu: 10m |
| 187 | + memory: 64Mi |
| 188 | + limits: |
| 189 | + cpu: 500m |
| 190 | + memory: 256Mi |
| 191 | + securityContext: |
| 192 | + allowPrivilegeEscalation: false |
| 193 | + readOnlyRootFilesystem: true |
| 194 | +`, ns, name, m.config.Version, m.config.Image, argsYAML) |
| 195 | +} |
| 196 | + |
| 197 | +// Cleanup removes all Reloader resources from the cluster. |
| 198 | +func (m *Manager) Cleanup(ctx context.Context) error { |
| 199 | + ns := m.namespace() |
| 200 | + name := m.releaseName() |
| 201 | + |
| 202 | + delDeploy := m.kubectl(ctx, "delete", "deployment", name, "-n", ns, "--ignore-not-found") |
| 203 | + delDeploy.Run() |
| 204 | + |
| 205 | + delCRB := m.kubectl(ctx, "delete", "clusterrolebinding", name, "--ignore-not-found") |
| 206 | + delCRB.Run() |
| 207 | + |
| 208 | + delCR := m.kubectl(ctx, "delete", "clusterrole", name, "--ignore-not-found") |
| 209 | + delCR.Run() |
| 210 | + |
| 211 | + delNS := m.kubectl(ctx, "delete", "namespace", ns, "--wait=false", "--ignore-not-found") |
| 212 | + if err := delNS.Run(); err != nil { |
| 213 | + return fmt.Errorf("deleting namespace: %w", err) |
| 214 | + } |
| 215 | + |
| 216 | + return nil |
| 217 | +} |
| 218 | + |
| 219 | +// CollectLogs collects logs from the Reloader pod and writes them to the specified file. |
| 220 | +func (m *Manager) CollectLogs(ctx context.Context, logPath string) error { |
| 221 | + ns := m.namespace() |
| 222 | + name := m.releaseName() |
| 223 | + |
| 224 | + if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil { |
| 225 | + return fmt.Errorf("creating log directory: %w", err) |
| 226 | + } |
| 227 | + |
| 228 | + cmd := m.kubectl(ctx, "logs", |
| 229 | + "-n", ns, |
| 230 | + "-l", fmt.Sprintf("app=%s", name), |
| 231 | + "--tail=-1") |
| 232 | + |
| 233 | + out, err := cmd.Output() |
| 234 | + if err != nil { |
| 235 | + cmd = m.kubectl(ctx, "logs", |
| 236 | + "-n", ns, |
| 237 | + "-l", "app.kubernetes.io/name=reloader", |
| 238 | + "--tail=-1") |
| 239 | + out, err = cmd.Output() |
| 240 | + if err != nil { |
| 241 | + return fmt.Errorf("collecting logs: %w", err) |
| 242 | + } |
| 243 | + } |
| 244 | + |
| 245 | + if err := os.WriteFile(logPath, out, 0644); err != nil { |
| 246 | + return fmt.Errorf("writing logs: %w", err) |
| 247 | + } |
| 248 | + |
| 249 | + return nil |
| 250 | +} |
0 commit comments