Deployment and lifecycle management for Nix
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

cli: merge seed and artifact

+66 -163
+4
apps/sower/lib/sower/seed.ex
··· 57 57 Repo.get_by(Seed, name: name, seed_type: seed_type) 58 58 end 59 59 60 + def get_sid(sid) do 61 + Repo.get_by(Seed, sid: sid) 62 + end 63 + 60 64 def get_sid!(sid) do 61 65 Repo.get_by!(Seed, sid: sid) 62 66 end
+7 -1
apps/sower/lib/sower_web/controllers/api/seed_controller.ex
··· 124 124 if conn.assigns.access_token 125 125 |> can() 126 126 |> read?(%Sower.Seed{org_id: conn.assigns.access_token.org_id}) do 127 - render(conn, :show, seed: Sower.Seed.get_sid!(sid)) 127 + case Sower.Seed.get_sid(sid) do 128 + nil -> 129 + conn |> put_status(404) |> render(:error, error: "not found") 130 + 131 + seed -> 132 + render(conn, :show, seed: seed) 133 + end 128 134 else 129 135 conn |> put_status(401) |> render(:error, error: "unauthorized") 130 136 end
+1 -1
bin/seed-ci
··· 150 150 # sower only understands certain build outputs 151 151 $configs | filter { |c| $c.seed_type == "home-manager" or $c.seed_type == "nix-darwin" or $c.seed_type == "nixos" } | each { |c| 152 152 $c.targets | filter { |t| $t.build != null } | each { |t| 153 - sower seed submit --create --name $t.name --type $c.seed_type --path $t.build.outputs.out.0 153 + sower seed submit --name $t.name --type $c.seed_type --path $t.build.outputs.out.0 154 154 } 155 155 } 156 156 }
+6 -51
client-go/seed_client.go
··· 14 14 client *ClientWithResponses 15 15 } 16 16 17 - func NewSeedClient(endpoint, token string) (*SeedClient, error) { 17 + func NewSowerClient(endpoint, token string) (*SeedClient, error) { 18 18 if token == "" { 19 19 return nil, fmt.Errorf("API token missing") 20 20 } ··· 36 36 }, nil 37 37 } 38 38 39 - func (s *SeedClient) CreateSeed(name, seedType string) (*Seed, error) { 39 + func (s *SeedClient) CreateSeed(name, seedType, artifact string) (*Seed, error) { 40 40 if name == "" || seedType == "" { 41 41 return nil, fmt.Errorf("seed name and type are required") 42 42 } ··· 46 46 return nil, err 47 47 } 48 48 49 - resp, err := s.client.NewSeedWithResponse(context.TODO(), Seed{Name: name, SeedType: st}) 49 + resp, err := s.client.NewSeedWithResponse(context.TODO(), Seed{Name: name, SeedType: st, Artifact: artifact}) 50 50 if err != nil { 51 51 return nil, err 52 52 } ··· 69 69 return seed, nil 70 70 } 71 71 72 - func (s *SeedClient) GetSeed(name, seedType string) (*Seed, error) { 72 + func (s *SeedClient) GetLatestSeed(name, seedType string) (*Seed, error) { 73 73 if name == "" || seedType == "" { 74 74 return nil, fmt.Errorf("seed name and type are required") 75 75 } ··· 80 80 return nil, fmt.Errorf("must specify both name and type") 81 81 } 82 82 83 - resp, err := s.client.ListSeedsWithResponse(context.TODO(), &ListSeedsParams{Name: &name, SeedType: &seedType}) 83 + resp, err := s.client.LatestSeedWithResponse(context.TODO(), &LatestSeedParams{Name: &name, SeedType: &seedType}) 84 84 if err != nil { 85 85 return nil, err 86 86 } ··· 97 97 return nil, fmt.Errorf("unknown error") 98 98 } 99 99 100 - newSeed = (*resp.JSON200)[0] 100 + newSeed = (*resp.JSON200) 101 101 102 102 slog.Debug("Found seed", "name", newSeed.Name, "type", newSeed.SeedType, "sid", *newSeed.Sid) 103 103 ··· 133 133 slog.Debug("Found seed", "name", newSeed.Name, "type", newSeed.SeedType, "sid", *newSeed.Sid) 134 134 135 135 return &newSeed, nil 136 - } 137 - 138 - func (s *SeedClient) GetSeedLatestPath(seed *Seed) (*StorePath, error) { 139 - resp, err := s.client.LatestStorePathBySeedWithResponse(context.TODO(), *seed.Sid) 140 - if err != nil { 141 - return nil, err 142 - } 143 - 144 - if resp.StatusCode() == http.StatusUnauthorized { 145 - return nil, fmt.Errorf("%s", *(*resp.JSON401).Error) 146 - } 147 - 148 - if resp.StatusCode() == http.StatusNotFound { 149 - return nil, fmt.Errorf("%s", *(*resp.JSON404).Error) 150 - } 151 - 152 - if resp.StatusCode() != http.StatusOK { 153 - return nil, fmt.Errorf("unknown error") 154 - } 155 - 156 - seedPath := resp.JSON200 157 - slog.Debug("Found path for seed", "path", seedPath.Path, "seed_sid", *seed.Sid) 158 - 159 - return seedPath, nil 160 - } 161 - 162 - func (s *SeedClient) SubmitSeedPath(seed *Seed, path string) (*StorePath, error) { 163 - resp, err := s.client.NewSeedStorePathWithResponse(context.TODO(), *seed.Sid, StorePath{Path: path}) 164 - if err != nil { 165 - return nil, err 166 - } 167 - 168 - if resp.StatusCode() == http.StatusUnauthorized { 169 - return nil, fmt.Errorf("%s", *(*resp.JSON401).Error) 170 - } 171 - 172 - if resp.StatusCode() != http.StatusCreated { 173 - return nil, fmt.Errorf("unknown error") 174 - } 175 - 176 - storePath := resp.JSON201 177 - 178 - slog.Debug("Created path for seed", "path", storePath, "sid", seed.Sid) 179 - 180 - return storePath, nil 181 136 } 182 137 183 138 func stringToSeedSeedType(s string) (SeedSeedType, error) {
+29 -91
cmd/cli/main.go
··· 54 54 } 55 55 56 56 type seedCmd struct { 57 - Create *seedCreateCmd `arg:"subcommand:create"` 58 57 Download *seedDownloadCmd `arg:"subcommand:download"` 59 58 Info *seedInfoCmd `arg:"subcommand:info"` 60 59 Reboot *seedRebootCmd `arg:"subcommand:reboot"` ··· 64 63 SeedType string `arg:"--type,-t" json:"type"` 65 64 Name string `arg:"--name,-n"` 66 65 } 67 - 68 - type seedCreateCmd struct{} 69 66 70 67 type seedDownloadCmd struct { 71 68 Initrd bool `arg:"--initrd"` ··· 78 75 } 79 76 80 77 type seedSubmitCmd struct { 81 - Path string `arg:"--path,-p,required"` 82 - Create bool `arg:"--create"` 78 + Artifact string `arg:"--path,-p,required"` 83 79 } 84 80 85 81 type seedUpgradeCmd struct { ··· 259 255 260 256 func seedSubcommand(cfg config) error { 261 257 switch { 262 - case cfg.Seed.Create != nil: 263 - seedClient, err := client.NewSeedClient(cfg.Endpoint, cfg.ApiToken) 264 - if err != nil { 265 - slog.Error("Failed to initialize seed client") 266 - os.Exit(1) 267 - } 268 - 269 - seed, err := seedClient.CreateSeed(cfg.Seed.Name, cfg.Seed.SeedType) 270 - if err != nil { 271 - slog.Error("Failed to create seed", "error", err) 272 - os.Exit(1) 273 - } 274 - 275 - slog.Info("Created seed", "name", seed.Name, "type", seed.SeedType, "sid", seed.Sid) 276 - 277 258 case cfg.Seed.Download != nil: 278 - seedClient, err := client.NewSeedClient(cfg.Endpoint, cfg.ApiToken) 259 + seedClient, err := client.NewSowerClient(cfg.Endpoint, cfg.ApiToken) 279 260 if err != nil { 280 261 slog.Error("Failed to initialize seed client") 281 262 os.Exit(1) 282 263 } 283 264 284 - seed, err := seedClient.GetSeed(cfg.Seed.Name, cfg.Seed.SeedType) 265 + seed, err := seedClient.GetLatestSeed(cfg.Seed.Name, cfg.Seed.SeedType) 285 266 if err != nil { 286 267 slog.Error("Failed to get seed", "error", err, "name", cfg.Seed.Name, "type", cfg.Seed.SeedType) 287 268 os.Exit(1) 288 269 } 289 270 290 - storePath, err := seedClient.GetSeedLatestPath(seed) 291 - if err != nil { 292 - slog.Error("Failed to get seed store path", "error", err) 293 - os.Exit(1) 294 - } 295 - 296 271 caches, err := seedClient.GetNixCaches() 297 272 if err != nil { 298 273 slog.Error("Failed to get nix caches", "error", err) 299 274 os.Exit(1) 300 275 } 301 276 302 - if err := realize(storePath.Path, caches, cfg.Seed.Download.Initrd, ""); err != nil { 277 + if err := realize(seed.Artifact, caches, cfg.Seed.Download.Initrd, ""); err != nil { 303 278 slog.Error("Failed realizing seed", "error", err) 304 279 os.Exit(1) 305 280 } 306 281 307 - slog.Info("Downloaded seed", "name", seed.Name, "type", seed.SeedType, "path", storePath.Path) 282 + slog.Info("Downloaded seed", "name", seed.Name, "type", seed.SeedType, "artifact", seed.Artifact) 308 283 309 284 case cfg.Seed.Info != nil: 310 - seedClient, err := client.NewSeedClient(cfg.Endpoint, cfg.ApiToken) 285 + seedClient, err := client.NewSowerClient(cfg.Endpoint, cfg.ApiToken) 311 286 if err != nil { 312 287 slog.Error("Failed to initialize seed client", "error", err) 313 288 os.Exit(1) 314 289 } 315 290 316 - seed, err := seedClient.GetSeed(cfg.Seed.Name, cfg.Seed.SeedType) 291 + seed, err := seedClient.GetLatestSeed(cfg.Seed.Name, cfg.Seed.SeedType) 317 292 if err != nil { 318 293 slog.Error("Failed to get seed", "error", err, "name", cfg.Seed.Name, "type", cfg.Seed.SeedType) 319 294 os.Exit(1) 320 295 } 321 296 322 - storePath, err := seedClient.GetSeedLatestPath(seed) 323 - if err != nil { 324 - slog.Error("Failed to get seed store path", "error", err) 325 - os.Exit(1) 326 - } 327 - 328 - slog.Info("Found seed", "name", seed.Name, "type", seed.SeedType, "path", storePath.Path) 297 + slog.Info("Found seed", "name", seed.Name, "type", seed.SeedType, "artifact", seed.Artifact) 329 298 330 299 case cfg.Seed.Reboot != nil: 331 300 err := reboot(cfg.Seed.Reboot.Yes) ··· 337 306 case cfg.Seed.Submit != nil: 338 307 cmdArgs := cfg.Seed.Submit 339 308 340 - err := preCheckSeed(cmdArgs.Path, cfg.Seed.SeedType) 309 + err := preCheckSeed(cmdArgs.Artifact, cfg.Seed.SeedType) 341 310 if err != nil { 342 311 slog.Error("Failed to pre-check seed for submission:", "error", err) 343 312 os.Exit(1) 344 313 } 345 314 346 - seedClient, err := client.NewSeedClient(cfg.Endpoint, cfg.ApiToken) 315 + seedClient, err := client.NewSowerClient(cfg.Endpoint, cfg.ApiToken) 347 316 if err != nil { 348 317 slog.Error("Failed to initialize seed client", "error", err) 349 318 os.Exit(1) ··· 351 320 352 321 var seed *client.Seed 353 322 354 - seed, err = seedClient.GetSeed(cfg.Seed.Name, cfg.Seed.SeedType) 355 - if err != nil && cmdArgs.Create { 356 - seed, err = seedClient.CreateSeed(cfg.Seed.Name, cfg.Seed.SeedType) 357 - if err != nil { 358 - slog.Error("Failed to create seed", "error", err) 359 - os.Exit(1) 360 - } 361 - } 362 - if err != nil { 363 - slog.Error("Failed to get seed", "error", err) 364 - os.Exit(1) 365 - } 366 - 367 - storePath, err := seedClient.SubmitSeedPath(seed, cmdArgs.Path) 323 + seed, err = seedClient.CreateSeed(cfg.Seed.Name, cfg.Seed.SeedType, cmdArgs.Artifact) 368 324 if err != nil { 369 - slog.Error("Failed submitting seed") 325 + slog.Error("Failed to create seed", "error", err) 370 326 os.Exit(1) 371 327 } 372 328 373 - slog.Info("Submitted seed", "name", seed.Name, "type", seed.SeedType, "path", storePath.Path) 329 + slog.Info("Submitted seed", "name", seed.Name, "type", seed.SeedType, "artifact", seed.Artifact) 374 330 375 331 case cfg.Seed.Upgrade != nil: 376 332 cmdArgs := cfg.Seed.Upgrade ··· 380 336 os.Exit(1) 381 337 } 382 338 383 - seedClient, err := client.NewSeedClient(cfg.Endpoint, cfg.ApiToken) 339 + seedClient, err := client.NewSowerClient(cfg.Endpoint, cfg.ApiToken) 384 340 if err != nil { 385 341 slog.Error("Failed to initialize seed client") 386 342 os.Exit(1) 387 343 } 388 344 389 - seed, err := seedClient.GetSeed(cfg.Seed.Name, cfg.Seed.SeedType) 345 + seed, err := seedClient.GetLatestSeed(cfg.Seed.Name, cfg.Seed.SeedType) 390 346 if err != nil { 391 347 slog.Error("Failed to get seed", "error", err) 392 348 os.Exit(1) 393 349 } 394 350 395 - storePath, err := seedClient.GetSeedLatestPath(seed) 396 - if err != nil { 397 - slog.Error("Failed to get seed store path", "error", err) 398 - os.Exit(1) 399 - } 400 - 401 351 caches, err := seedClient.GetNixCaches() 402 352 if err != nil { 403 353 slog.Error("Failed to get nix caches", "error", err) 404 354 os.Exit(1) 405 355 } 406 356 407 - if err := realize(storePath.Path, caches, false, ""); err != nil { 357 + if err := realize(seed.Artifact, caches, false, ""); err != nil { 408 358 slog.Error("Failed realizing seed", "error", err) 409 359 os.Exit(1) 410 360 } 411 361 412 - if err := activate(seed.SeedType, storePath.Path, cmdArgs.Mode); err != nil { 362 + if err := activate(seed.SeedType, seed.Artifact, cmdArgs.Mode); err != nil { 413 363 slog.Error("Failed realizing seed", "error", err) 414 364 os.Exit(1) 415 365 } 416 366 417 - slog.Info("Upgraded seed", "name", cfg.Seed.Name, "type", seed.SeedType, "path", storePath.Path) 367 + slog.Info("Upgraded seed", "name", cfg.Seed.Name, "type", seed.SeedType, "artifact", seed.Artifact) 418 368 419 369 if seed.SeedType == client.Nixos { 420 370 err := reboot(cmdArgs.Yes) ··· 438 388 439 389 switch { 440 390 case cfg.Services.List != nil: 441 - seedClient, err := client.NewSeedClient(cfg.Endpoint, cfg.ApiToken) 391 + seedClient, err := client.NewSowerClient(cfg.Endpoint, cfg.ApiToken) 442 392 if err != nil { 443 393 slog.Error("Failed to initialize seed client") 444 394 os.Exit(1) 445 395 } 446 396 447 397 for _, service := range cfg.Services.Services { 448 - seed, err := seedClient.GetSeed(string(service), string(client.Service)) 398 + seed, err := seedClient.GetLatestSeed(string(service), string(client.Service)) 449 399 if err != nil { 450 400 slog.Error("Failed to get seed", "error", err, "name", string(service), "type", client.Service) 451 401 continue 452 402 } 453 403 454 - storePath, err := seedClient.GetSeedLatestPath(seed) 455 - if err != nil { 456 - slog.Error("Failed to get seed store path", "error", err) 457 - continue 458 - } 459 - 460 - slog.Info("Found service", "service", service, "store_path", storePath.Path) 404 + slog.Info("Found service", "service", service, "artifact", seed.Artifact) 461 405 } 462 406 case cfg.Services.Upgrade != nil: 463 - seedClient, err := client.NewSeedClient(cfg.Endpoint, cfg.ApiToken) 407 + seedClient, err := client.NewSowerClient(cfg.Endpoint, cfg.ApiToken) 464 408 if err != nil { 465 409 slog.Error("Failed to initialize seed client") 466 410 os.Exit(1) 467 411 } 468 412 469 - paths := []client.StorePath{} 413 + seeds := []client.Seed{} 470 414 471 415 if len(cfg.Services.Services) == 0 { 472 416 slog.Error("No services configured") ··· 485 429 } 486 430 487 431 for _, service := range cfg.Services.Services { 488 - seed, err := seedClient.GetSeed(string(service), string(client.Service)) 432 + seed, err := seedClient.GetLatestSeed(string(service), string(client.Service)) 489 433 if err != nil { 490 434 slog.Error("Failed to get seed", "error", err, "name", string(service), "type", client.Service) 491 435 continue 492 436 } 493 437 494 - storePath, err := seedClient.GetSeedLatestPath(seed) 495 - if err != nil { 496 - slog.Error("Failed to get seed store path", "error", err) 497 - continue 498 - } 499 - 500 - slog.Info("Found service", "service", service, "store_path", storePath.Path) 438 + slog.Info("Found service", "service", service, "artifact", seed.Artifact) 501 439 502 440 caches, err := seedClient.GetNixCaches() 503 441 if err != nil { ··· 505 443 os.Exit(1) 506 444 } 507 445 508 - if err := realize(storePath.Path, caches, false, filepath.Join(servicesProfileDir, string(service))); err != nil { 446 + if err := realize(seed.Artifact, caches, false, filepath.Join(servicesProfileDir, string(service))); err != nil { 509 447 slog.Error("Failed realizing seed", "error", err) 510 448 os.Exit(1) 511 449 } 512 450 513 - slog.Info("Downloaded seed", "name", seed.Name, "type", seed.SeedType, "path", storePath.Path) 451 + slog.Info("Downloaded seed", "name", seed.Name, "type", seed.SeedType, "artifact", seed.Artifact) 514 452 515 - paths = append(paths, *storePath) 453 + seeds = append(seeds, *seed) 516 454 } 517 455 518 - servicesPath, err := buildServicesUnits(paths) 456 + servicesPath, err := buildServicesUnits(seeds) 519 457 if err != nil { 520 458 slog.Error("Failed to build services environment", "error", err) 521 459 os.Exit(1)
+13 -13
cmd/cli/services.go
··· 18 18 var nixpkgsref = "refs/heads/nixos-unstable" 19 19 20 20 type ServicesManifest struct { 21 - Inputs []client.StorePath `json:"inputs"` 21 + Seeds []client.Seed `json:"seeds"` 22 22 } 23 23 24 24 // https://github.com/NixOS/nixpkgs/archive/refs/heads/master.zip 25 - func buildServicesUnits(paths []client.StorePath) (string, error) { 26 - if len(paths) == 0 { 27 - return "", fmt.Errorf("no paths specified") 25 + func buildServicesUnits(seeds []client.Seed) (string, error) { 26 + if len(seeds) == 0 { 27 + return "", fmt.Errorf("no seeds specified") 28 28 } 29 29 30 - profileDir := filepath.Join(profileParentDir(), "services", servicesHash(paths)) 30 + profileDir := filepath.Join(profileParentDir(), "services", servicesHash(seeds)) 31 31 manifest := &ServicesManifest{} 32 32 slog.Debug("Collecting services units", "profile", profileDir) 33 33 ··· 40 40 } 41 41 } 42 42 43 - for _, path := range paths { 44 - sourceDir := filepath.Join(path.Path, ".sower", "systemd") 43 + for _, seed := range seeds { 44 + sourceDir := filepath.Join(seed.Artifact, ".sower", "systemd") 45 45 46 46 // revisit in 1.25 to see if the CopyFS behavior changes, but currently fails on symlinks 47 47 // err = os.CopyFS(profileDir, os.DirFS(sourceDir)) ··· 49 49 cmd := exec.Command("cp", "--recursive", "--no-clobber", sourceDir, profileDir) 50 50 err = cmd.Run() 51 51 if err != nil { 52 - return "", fmt.Errorf("failed to copy path %s to profile %s: %v", path.Path, profileDir, err) 52 + return "", fmt.Errorf("failed to copy path %s to profile %s: %v", seed.Artifact, profileDir, err) 53 53 } 54 54 55 - manifest.Inputs = append(manifest.Inputs, path) 55 + manifest.Seeds = append(manifest.Seeds, seed) 56 56 } 57 57 58 58 data, err := json.MarshalIndent(manifest, "", " ") ··· 69 69 return profileDir, nil 70 70 } 71 71 72 - func servicesHash(paths []client.StorePath) string { 72 + func servicesHash(seeds []client.Seed) string { 73 73 hash := sha256.New() 74 - for _, path := range paths { 75 - hash.Write([]byte(path.Path)) 74 + for _, seed := range seeds { 75 + hash.Write([]byte(seed.Artifact)) 76 76 } 77 77 78 78 return hex.EncodeToString(hash.Sum(nil)) ··· 91 91 } 92 92 err = os.MkdirAll(filepath.Join(oldProfile, "systemd", "system"), 0755) 93 93 if err != nil { 94 - return fmt.Errorf("failed to create fake systemd dir: %v", err) 94 + return fmt.Errorf("failed to seed.Artifact dir: %v", err) 95 95 } 96 96 } else { 97 97 oldProfile, err = filepath.EvalSymlinks(profilePath)
+3 -3
justfile
··· 31 31 mix run apps/sower/priv/repo/seeds-user.exs {{ email }} --no-start 32 32 33 33 dev-seed-from-local: 34 - go run ./cmd/cli seed submit --create --name $(hostname -s) --type nixos --path $(readlink -f /run/booted-system) 35 - go run ./cmd/cli seed submit --create --name $(hostname -s) --type nixos --path $(readlink -f /run/current-system) 36 - go run ./cmd/cli seed submit --create --name $(hostname -s) --type home-manager --path $(readlink -f $HOME/.local/state/nix/profiles/home-manager) 34 + go run ./cmd/cli seed submit --name $(hostname -s) --type nixos --path $(readlink -f /run/booted-system) 35 + go run ./cmd/cli seed submit --name $(hostname -s) --type nixos --path $(readlink -f /run/current-system) 36 + go run ./cmd/cli seed submit --name $(hostname -s) --type home-manager --path $(readlink -f $HOME/.local/state/nix/profiles/home-manager) 37 37 38 38 dev-services: 39 39 process-compose list || process-compose up --detached
+3 -3
nix/tests/e2e.nix
··· 141 141 142 142 with subtest("basic submission"): 143 143 server_profile = server.succeed("readlink -f /run/booted-system").strip() 144 - server.succeed(f"sower seed submit --create --path {server_profile} --debug") 144 + server.succeed(f"sower seed submit --path {server_profile} --debug") 145 145 146 146 client_profile = client.succeed("readlink -f /run/booted-system").strip() 147 - server.succeed(f"sower seed submit --create --name client --type nixos --path {client_profile} --debug") 147 + server.succeed(f"sower seed submit --name client --type nixos --path {client_profile} --debug") 148 148 149 - server.succeed("sower seed submit --create --name simple-service --type service --path ${simple-service} --debug") 149 + server.succeed("sower seed submit --name simple-service --type service --path ${simple-service} --debug") 150 150 151 151 with subtest("activate seed"): 152 152 server.succeed("sower seed upgrade --debug")