1package auto
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "strings"
9
10 "github.com/bnema/gordon/internal/boundaries/out"
11 "github.com/bnema/gordon/internal/domain"
12)
13
14// manifestSchema represents the relevant parts of an OCI/Docker manifest.
15type manifestSchema struct {
16 SchemaVersion int `json:"schemaVersion"`
17 MediaType string `json:"mediaType"`
18 Config struct {
19 MediaType string `json:"mediaType"`
20 Digest string `json:"digest"`
21 Size int64 `json:"size"`
22 } `json:"config"`
23}
24
25// ParseConfigDigest extracts the config digest from a manifest.
26func ParseConfigDigest(manifestData []byte) (string, error) {
27 var manifest manifestSchema
28 if err := json.Unmarshal(manifestData, &manifest); err != nil {
29 return "", err
30 }
31
32 return manifest.Config.Digest, nil
33}
34
35// imageConfig represents the relevant parts of an OCI/Docker image config.
36type imageConfig struct {
37 Config struct {
38 Labels map[string]string `json:"Labels"`
39 } `json:"config"`
40}
41
42// ParseImageLabels extracts Gordon labels from an image config blob.
43func ParseImageLabels(configData []byte) (*domain.ImageLabels, error) {
44 var config imageConfig
45 if err := json.Unmarshal(configData, &config); err != nil {
46 return nil, err
47 }
48
49 labels := &domain.ImageLabels{}
50
51 if config.Config.Labels == nil {
52 return labels, nil
53 }
54
55 // Extract gordon.* labels
56 if v, ok := config.Config.Labels[domain.LabelDomain]; ok {
57 labels.Domain = strings.TrimSpace(v)
58 }
59
60 if v, ok := config.Config.Labels[domain.LabelDomains]; ok {
61 // Parse comma-separated domains
62 for _, d := range strings.Split(v, ",") {
63 d = strings.TrimSpace(d)
64 if d != "" {
65 labels.Domains = append(labels.Domains, d)
66 }
67 }
68 }
69
70 if v, ok := config.Config.Labels[domain.LabelHealth]; ok {
71 labels.Health = strings.TrimSpace(v)
72 }
73
74 for _, key := range []string{domain.LabelProxyPort, domain.LabelPort} {
75 if v, ok := config.Config.Labels[key]; ok {
76 labels.Port = strings.TrimSpace(v)
77 break
78 }
79 }
80
81 if v, ok := config.Config.Labels[domain.LabelEnvFile]; ok {
82 labels.EnvFile = strings.TrimSpace(v)
83 }
84
85 return labels, nil
86}
87
88// ExtractLabels extracts Gordon labels from an image manifest by fetching the
89// config blob from blobStorage and parsing it. It is the shared implementation
90// used by both AutoRouteHandler and AutoPreviewHandler.
91// ctx is accepted for future use when BlobStorage gains context support.
92func ExtractLabels(ctx context.Context, manifestData []byte, blobStorage out.BlobStorage) (*domain.ImageLabels, error) {
93 configDigest, err := ParseConfigDigest(manifestData)
94 if err != nil {
95 return nil, fmt.Errorf("failed to parse manifest: %w", err)
96 }
97
98 if configDigest == "" {
99 return nil, fmt.Errorf("no config digest found in manifest")
100 }
101
102 reader, err := blobStorage.GetBlob(configDigest)
103 if err != nil {
104 return nil, fmt.Errorf("failed to get config blob: %w", err)
105 }
106 defer reader.Close()
107
108 configData, err := io.ReadAll(reader)
109 if err != nil {
110 return nil, fmt.Errorf("failed to read config blob: %w", err)
111 }
112
113 labels, err := ParseImageLabels(configData)
114 if err != nil {
115 return nil, fmt.Errorf("failed to parse config: %w", err)
116 }
117
118 return labels, nil
119}