Deployment and lifecycle management for Nix
0
fork

Configure Feed

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

cli: support submission of seed tags

+258 -63
+3 -1
apps/sower/lib/sower/seed.ex
··· 7 7 alias Sower.{Repo, Seed, SeedTag} 8 8 alias Ecto.Multi 9 9 10 - @derive {Jason.Encoder, only: [:sid, :name, :seed_type, :artifact]} 10 + @derive {Jason.Encoder, only: [:sid, :name, :seed_type, :artifact, :tags]} 11 11 12 12 @derive {Phoenix.Param, key: :sid} 13 13 ··· 27 27 end 28 28 29 29 def create(attrs) do 30 + dbg(attrs) 31 + 30 32 Multi.new() 31 33 |> Multi.insert(:seed, changeset(%Seed{org_id: Sower.Repo.get_org_id()}, attrs), 32 34 on_conflict: {:replace, [:updated_at]},
+2
apps/sower/lib/sower/seed_tag.ex
··· 2 2 use Sower.Schema 3 3 import Ecto.Changeset 4 4 5 + @derive {Jason.Encoder, only: [:key, :value]} 6 + 5 7 schema "seed_tags" do 6 8 field :key, :string 7 9 field :value, :string
+15 -2
apps/sower/lib/sower_web/controllers/api/seed_controller.ex
··· 33 33 body_params: %Schemas.Seed{ 34 34 name: name, 35 35 seed_type: seed_type, 36 - artifact: artifact 36 + artifact: artifact, 37 + tags: tags 37 38 } 38 39 } = conn, 39 40 _params ··· 42 43 43 44 if can(conn.assigns.access_token) 44 45 |> create?(%Sower.Seed{org_id: conn.assigns.access_token.org_id}) do 45 - case Sower.Seed.create(%{name: name, seed_type: seed_type, artifact: artifact}) do 46 + seed_attrs = %{name: name, seed_type: seed_type, artifact: artifact} 47 + 48 + seed_attrs = 49 + case tags do 50 + nil -> 51 + seed_attrs 52 + 53 + tags when is_list(tags) -> 54 + Map.put(seed_attrs, :tags, Enum.map(tags, &Map.from_struct/1)) 55 + end 56 + |> dbg() 57 + 58 + case Sower.Seed.create(seed_attrs) do 46 59 {:ok, %Sower.Seed{} = seed} -> 47 60 conn 48 61 |> put_status(:created)
+7 -1
apps/sower_client/lib/schemas/seed.ex
··· 25 25 artifact: %Schema{ 26 26 type: :string, 27 27 description: "Artifact of the seed" 28 + }, 29 + tags: %Schema{ 30 + type: :array, 31 + description: "Tags associated with the seed", 32 + items: SowerClient.Schemas.SeedTag 28 33 } 29 34 }, 30 35 required: [:name, :seed_type, :artifact], ··· 32 37 "sid" => "example4ser3adju75ddusbr", 33 38 "name" => "myhost", 34 39 "seed_type" => "nixos", 35 - "artifact" => "/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-nixos" 40 + "artifact" => "/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-nixos", 41 + "tags" => [] 36 42 } 37 43 }) 38 44
+24
apps/sower_client/lib/schemas/seed_tag.ex
··· 1 + defmodule SowerClient.Schemas.SeedTag do 2 + use SowerClient.Schema 3 + 4 + OpenApiSpex.schema(%{ 5 + title: "SeedTag", 6 + description: "A tag associated with a seed", 7 + type: :object, 8 + properties: %{ 9 + key: %Schema{ 10 + type: :string, 11 + description: "Tag key" 12 + }, 13 + value: %Schema{ 14 + type: :string, 15 + description: "Tag value" 16 + } 17 + }, 18 + required: [:key, :value], 19 + example: %{ 20 + "key" => "environment", 21 + "value" => "production" 22 + } 23 + }) 24 + end
+12
client-go/client.gen.go
··· 53 53 54 54 // Sid sid of the seed set by the server 55 55 Sid *string `json:"sid,omitempty"` 56 + 57 + // Tags Tags associated with the seed 58 + Tags *[]SeedTag `json:"tags,omitempty"` 56 59 } 57 60 58 61 // SeedSeedType Type of the seed 59 62 type SeedSeedType string 63 + 64 + // SeedTag A tag associated with a seed 65 + type SeedTag struct { 66 + // Key Tag key 67 + Key string `json:"key"` 68 + 69 + // Value Tag value 70 + Value string `json:"value"` 71 + } 60 72 61 73 // ListSeedsParams defines parameters for ListSeeds. 62 74 type ListSeedsParams struct {
-48
client-go/end_to_end_test.go
··· 1 - package client_test 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "net/http" 7 - "testing" 8 - 9 - "codeberg.org/adamcstephens/sower/client-go" 10 - ) 11 - 12 - // custom HTTP client 13 - var hc = http.Client{} 14 - 15 - func TestClient_RawRequest(t *testing.T) { 16 - 17 - // with a raw http.Response 18 - c, err := client.NewClient("http://localhost:7150", client.WithHTTPClient(&hc)) 19 - if err != nil { 20 - t.Fatal(err) 21 - } 22 - 23 - resp, err := c.ListSeeds(context.TODO(), &client.ListSeedsParams{}) 24 - if err != nil { 25 - t.Fatal(err) 26 - } 27 - if resp.StatusCode != http.StatusOK { 28 - t.Fatalf("Expected HTTP 200 but received %d", resp.StatusCode) 29 - } 30 - } 31 - 32 - func TestClient_SeedList(t *testing.T) { 33 - // or to get a struct with the parsed response body 34 - c, err := client.NewClientWithResponses("http://localhost:7150", client.WithHTTPClient(&hc)) 35 - if err != nil { 36 - t.Fatal(err) 37 - } 38 - 39 - resp, err := c.ListSeedsWithResponse(context.TODO(), &client.ListSeedsParams{}) 40 - if err != nil { 41 - t.Fatal(err) 42 - } 43 - if resp.StatusCode() != http.StatusOK { 44 - t.Fatalf("Expected HTTP 200 but received %d", resp.StatusCode()) 45 - } 46 - 47 - fmt.Printf("resp.JSON200: %v\n", resp.JSON200) 48 - }
+10 -5
client-go/seed_client.go
··· 36 36 }, nil 37 37 } 38 38 39 - func (s *SeedClient) CreateSeed(name, seedType, artifact string) (*Seed, error) { 39 + func (s *SeedClient) CreateSeed(name, seedType, artifact string, tags []SeedTag) (*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, Artifact: artifact}) 49 + seed := Seed{Name: name, SeedType: st, Artifact: artifact} 50 + if len(tags) > 0 { 51 + seed.Tags = &tags 52 + } 53 + 54 + resp, err := s.client.NewSeedWithResponse(context.TODO(), seed) 50 55 if err != nil { 51 56 return nil, err 52 57 } ··· 63 68 return nil, fmt.Errorf("unknown error") 64 69 } 65 70 66 - seed := resp.JSON201 67 - slog.Debug("Created seed", "sid", seed.Sid) 71 + seed_resp := resp.JSON201 72 + slog.Debug("Created seed", "sid", seed_resp.Sid) 68 73 69 - return seed, nil 74 + return seed_resp, nil 70 75 } 71 76 72 77 func (s *SeedClient) GetLatestSeed(name, seedType string) (*Seed, error) {
+16 -2
cmd/cli/main.go
··· 81 81 } 82 82 83 83 type seedSubmitCmd struct { 84 - Artifact string `arg:"--path,-p,required"` 84 + Artifact string `arg:"--path,-p,required"` 85 + Tags []string `arg:"--tag,separate" help:"Tags in key=value format. Can be repeated."` 85 86 } 86 87 87 88 type seedUpgradeCmd struct { ··· 330 331 os.Exit(1) 331 332 } 332 333 334 + var tags []client.SeedTag 335 + for _, tagStr := range cmdArgs.Tags { 336 + parts := strings.SplitN(tagStr, "=", 2) 337 + if len(parts) != 2 { 338 + slog.Error("Invalid tag format. Expected key=value", "tag", tagStr) 339 + os.Exit(1) 340 + } 341 + tags = append(tags, client.SeedTag{ 342 + Key: parts[0], 343 + Value: parts[1], 344 + }) 345 + } 346 + 333 347 var seed *client.Seed 334 348 335 - seed, err = seedClient.CreateSeed(cfg.Seed.Name, cfg.Seed.SeedType, cmdArgs.Artifact) 349 + seed, err = seedClient.CreateSeed(cfg.Seed.Name, cfg.Seed.SeedType, cmdArgs.Artifact, tags) 336 350 if err != nil { 337 351 slog.Error("Failed to create seed", "error", err) 338 352 os.Exit(1)
+123
cmd/cli/seed_test.go
··· 1 + package main 2 + 3 + import ( 4 + "strings" 5 + "testing" 6 + 7 + "codeberg.org/adamcstephens/sower/client-go" 8 + ) 9 + 10 + func TestTagParsing(t *testing.T) { 11 + tests := []struct { 12 + name string 13 + input []string 14 + wantTags []client.SeedTag 15 + wantErr bool 16 + errContains string 17 + }{ 18 + { 19 + name: "single tag", 20 + input: []string{"environment=production"}, 21 + wantTags: []client.SeedTag{ 22 + {Key: "environment", Value: "production"}, 23 + }, 24 + wantErr: false, 25 + }, 26 + { 27 + name: "multiple tags", 28 + input: []string{"environment=production", "version=1.2.3", "region=us-west-2"}, 29 + wantTags: []client.SeedTag{ 30 + {Key: "environment", Value: "production"}, 31 + {Key: "version", Value: "1.2.3"}, 32 + {Key: "region", Value: "us-west-2"}, 33 + }, 34 + wantErr: false, 35 + }, 36 + { 37 + name: "empty tags", 38 + input: []string{}, 39 + wantTags: []client.SeedTag{}, 40 + wantErr: false, 41 + }, 42 + { 43 + name: "invalid tag format - no equals", 44 + input: []string{"invalidtag"}, 45 + wantTags: nil, 46 + wantErr: true, 47 + errContains: "Invalid tag format", 48 + }, 49 + { 50 + name: "invalid tag format - multiple equals", 51 + input: []string{"key=value=extra"}, 52 + wantTags: []client.SeedTag{{Key: "key", Value: "value=extra"}}, 53 + wantErr: false, 54 + errContains: "", 55 + }, 56 + { 57 + name: "tag with empty value", 58 + input: []string{"key="}, 59 + wantTags: []client.SeedTag{ 60 + {Key: "key", Value: ""}, 61 + }, 62 + wantErr: false, 63 + }, 64 + { 65 + name: "tag with empty key", 66 + input: []string{"=value"}, 67 + wantTags: []client.SeedTag{{Key: "", Value: "value"}}, 68 + wantErr: false, 69 + errContains: "", 70 + }, 71 + } 72 + 73 + for _, tt := range tests { 74 + t.Run(tt.name, func(t *testing.T) { 75 + var tags []client.SeedTag 76 + var err error 77 + 78 + for _, tagStr := range tt.input { 79 + parts := strings.SplitN(tagStr, "=", 2) 80 + if len(parts) != 2 { 81 + err = &TagParseError{Tag: tagStr} 82 + break 83 + } 84 + tags = append(tags, client.SeedTag{ 85 + Key: parts[0], 86 + Value: parts[1], 87 + }) 88 + } 89 + 90 + if tt.wantErr { 91 + if err == nil { 92 + t.Errorf("expected error but got none") 93 + } 94 + return 95 + } 96 + 97 + if err != nil { 98 + t.Errorf("unexpected error: %v", err) 99 + return 100 + } 101 + 102 + if len(tags) != len(tt.wantTags) { 103 + t.Errorf("got %d tags, want %d", len(tags), len(tt.wantTags)) 104 + return 105 + } 106 + 107 + for i, tag := range tags { 108 + if tag.Key != tt.wantTags[i].Key || tag.Value != tt.wantTags[i].Value { 109 + t.Errorf("tag[%d]: got {Key: %q, Value: %q}, want {Key: %q, Value: %q}", 110 + i, tag.Key, tag.Value, tt.wantTags[i].Key, tt.wantTags[i].Value) 111 + } 112 + } 113 + }) 114 + } 115 + } 116 + 117 + type TagParseError struct { 118 + Tag string 119 + } 120 + 121 + func (e *TagParseError) Error() string { 122 + return "Invalid tag format. Expected key=value" 123 + }
+6 -3
justfile
··· 12 12 check-elixir-test: dev-services 13 13 mix test 14 14 15 - check-go: check-go-lint 15 + check-go: check-go-lint check-go-test 16 16 17 17 check-go-lint: 18 18 golangci-lint run 19 + 20 + check-go-test: 21 + go test ./... 19 22 20 23 check-nix: 21 24 nix build .#checks.x86_64-linux.default --print-build-logs ··· 30 33 mix run apps/sower/priv/repo/seeds-user.exs {{ email }} --no-start 31 34 32 35 dev-seed-from-local: 33 - go run ./cmd/cli seed submit --name $(hostname -s) --type nixos --path $(readlink -f /run/current-system) 34 - go run ./cmd/cli seed submit --name $(hostname -s) --type home-manager --path $(readlink -f $HOME/.local/state/nix/profiles/home-manager) 36 + go run ./cmd/cli seed submit --name $(hostname -s) --type nixos --path $(readlink -f /run/current-system) --tag source=dev --tag test=anotherval 37 + go run ./cmd/cli seed submit --name $(hostname -s) --type home-manager --path $(readlink -f $HOME/.local/state/nix/profiles/home-manager) --tag source=dev 35 38 36 39 dev-services: 37 40 process-compose list || process-compose up --detached
+40 -1
openapi.json
··· 45 45 "artifact": "/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-nixos", 46 46 "name": "myhost", 47 47 "seed_type": "nixos", 48 - "sid": "example4ser3adju75ddusbr" 48 + "sid": "example4ser3adju75ddusbr", 49 + "tags": [] 49 50 }, 50 51 "properties": { 51 52 "artifact": { ··· 78 79 "type": "string", 79 80 "x-struct": null, 80 81 "x-validate": null 82 + }, 83 + "tags": { 84 + "description": "Tags associated with the seed", 85 + "items": { 86 + "$ref": "#/components/schemas/SeedTag" 87 + }, 88 + "type": "array", 89 + "x-struct": null, 90 + "x-validate": null 81 91 } 82 92 }, 83 93 "required": [ ··· 88 98 "title": "Seed", 89 99 "type": "object", 90 100 "x-struct": "Elixir.SowerClient.Schemas.Seed", 101 + "x-validate": null 102 + }, 103 + "SeedTag": { 104 + "description": "A tag associated with a seed", 105 + "example": { 106 + "key": "environment", 107 + "value": "production" 108 + }, 109 + "properties": { 110 + "key": { 111 + "description": "Tag key", 112 + "type": "string", 113 + "x-struct": null, 114 + "x-validate": null 115 + }, 116 + "value": { 117 + "description": "Tag value", 118 + "type": "string", 119 + "x-struct": null, 120 + "x-validate": null 121 + } 122 + }, 123 + "required": [ 124 + "key", 125 + "value" 126 + ], 127 + "title": "SeedTag", 128 + "type": "object", 129 + "x-struct": "Elixir.SowerClient.Schemas.SeedTag", 91 130 "x-validate": null 92 131 } 93 132 },