Skip to content

Commit 3b67860

Browse files
authored
Merge pull request #65 from flatrun/feat/track-registry-credentials
feat(credentials): track registry credentials for private deployments
2 parents bb9b385 + b553293 commit 3b67860

3 files changed

Lines changed: 176 additions & 3 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"strings"
6+
"testing"
7+
8+
"github.com/flatrun/agent/pkg/models"
9+
"gopkg.in/yaml.v3"
10+
)
11+
12+
func TestServiceMetadataCredentialID(t *testing.T) {
13+
metadata := &models.ServiceMetadata{
14+
Name: "test-deployment",
15+
Type: "custom",
16+
Networking: models.NetworkingConfig{
17+
Expose: true,
18+
Domain: "test.example.com",
19+
ContainerPort: 8080,
20+
},
21+
CredentialID: "cred-123",
22+
}
23+
24+
if metadata.CredentialID != "cred-123" {
25+
t.Errorf("expected CredentialID 'cred-123', got '%s'", metadata.CredentialID)
26+
}
27+
}
28+
29+
func TestServiceMetadataCredentialIDEmpty(t *testing.T) {
30+
metadata := &models.ServiceMetadata{
31+
Name: "public-deployment",
32+
Type: "custom",
33+
}
34+
35+
if metadata.CredentialID != "" {
36+
t.Errorf("expected empty CredentialID for public deployment, got '%s'", metadata.CredentialID)
37+
}
38+
}
39+
40+
func TestServiceMetadataCredentialIDOmitempty(t *testing.T) {
41+
metadata := &models.ServiceMetadata{
42+
Name: "test",
43+
Type: "custom",
44+
}
45+
46+
if metadata.CredentialID != "" {
47+
t.Error("CredentialID should be empty by default")
48+
}
49+
50+
metadata.CredentialID = "new-cred-id"
51+
if metadata.CredentialID != "new-cred-id" {
52+
t.Errorf("expected CredentialID 'new-cred-id', got '%s'", metadata.CredentialID)
53+
}
54+
}
55+
56+
func TestServiceMetadataCredentialIDYAMLSerialization(t *testing.T) {
57+
metadata := &models.ServiceMetadata{
58+
Name: "private-app",
59+
Type: "custom",
60+
CredentialID: "cred-abc-123",
61+
}
62+
63+
data, err := yaml.Marshal(metadata)
64+
if err != nil {
65+
t.Fatalf("failed to marshal metadata: %v", err)
66+
}
67+
68+
yamlStr := string(data)
69+
if !strings.Contains(yamlStr, "credential_id: cred-abc-123") {
70+
t.Errorf("YAML should contain credential_id field, got:\n%s", yamlStr)
71+
}
72+
73+
var unmarshaled models.ServiceMetadata
74+
if err := yaml.Unmarshal(data, &unmarshaled); err != nil {
75+
t.Fatalf("failed to unmarshal metadata: %v", err)
76+
}
77+
78+
if unmarshaled.CredentialID != "cred-abc-123" {
79+
t.Errorf("expected CredentialID 'cred-abc-123' after unmarshal, got '%s'", unmarshaled.CredentialID)
80+
}
81+
}
82+
83+
func TestServiceMetadataCredentialIDYAMLOmitsEmpty(t *testing.T) {
84+
metadata := &models.ServiceMetadata{
85+
Name: "public-app",
86+
Type: "custom",
87+
}
88+
89+
data, err := yaml.Marshal(metadata)
90+
if err != nil {
91+
t.Fatalf("failed to marshal metadata: %v", err)
92+
}
93+
94+
yamlStr := string(data)
95+
if strings.Contains(yamlStr, "credential_id") {
96+
t.Errorf("YAML should omit credential_id when empty, got:\n%s", yamlStr)
97+
}
98+
}
99+
100+
func TestServiceMetadataCredentialIDJSONSerialization(t *testing.T) {
101+
metadata := &models.ServiceMetadata{
102+
Name: "private-app",
103+
Type: "custom",
104+
CredentialID: "cred-xyz-789",
105+
}
106+
107+
data, err := json.Marshal(metadata)
108+
if err != nil {
109+
t.Fatalf("failed to marshal metadata: %v", err)
110+
}
111+
112+
jsonStr := string(data)
113+
if !strings.Contains(jsonStr, `"credential_id":"cred-xyz-789"`) {
114+
t.Errorf("JSON should contain credential_id field, got:\n%s", jsonStr)
115+
}
116+
117+
var unmarshaled models.ServiceMetadata
118+
if err := json.Unmarshal(data, &unmarshaled); err != nil {
119+
t.Fatalf("failed to unmarshal metadata: %v", err)
120+
}
121+
122+
if unmarshaled.CredentialID != "cred-xyz-789" {
123+
t.Errorf("expected CredentialID 'cred-xyz-789' after unmarshal, got '%s'", unmarshaled.CredentialID)
124+
}
125+
}
126+
127+
func TestServiceMetadataCredentialIDJSONOmitsEmpty(t *testing.T) {
128+
metadata := &models.ServiceMetadata{
129+
Name: "public-app",
130+
Type: "custom",
131+
}
132+
133+
data, err := json.Marshal(metadata)
134+
if err != nil {
135+
t.Fatalf("failed to marshal metadata: %v", err)
136+
}
137+
138+
jsonStr := string(data)
139+
if strings.Contains(jsonStr, "credential_id") {
140+
t.Errorf("JSON should omit credential_id when empty, got:\n%s", jsonStr)
141+
}
142+
}

internal/api/server.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@ import (
1919
"github.com/flatrun/agent/internal/audit"
2020
"github.com/flatrun/agent/internal/auth"
2121
"github.com/flatrun/agent/internal/backup"
22-
"github.com/flatrun/agent/internal/dns"
23-
dnsPlugins "github.com/flatrun/agent/pkg/plugins/dns"
2422
"github.com/flatrun/agent/internal/certs"
2523
"github.com/flatrun/agent/internal/credentials"
2624
"github.com/flatrun/agent/internal/database"
25+
"github.com/flatrun/agent/internal/dns"
2726
"github.com/flatrun/agent/internal/docker"
2827
"github.com/flatrun/agent/internal/files"
2928
"github.com/flatrun/agent/internal/infra"
@@ -36,6 +35,7 @@ import (
3635
"github.com/flatrun/agent/pkg/config"
3736
"github.com/flatrun/agent/pkg/models"
3837
"github.com/flatrun/agent/pkg/plugins"
38+
dnsPlugins "github.com/flatrun/agent/pkg/plugins/dns"
3939
"github.com/flatrun/agent/pkg/subdomain"
4040
"github.com/flatrun/agent/pkg/version"
4141
"github.com/flatrun/agent/templates"
@@ -705,10 +705,12 @@ func (s *Server) createDeployment(c *gin.Context) {
705705
}
706706

707707
var registryLoginError string
708+
var credentialID string
708709
if req.RegistryCredential != nil {
709710
var username, password string
710711

711712
if req.RegistryCredential.CredentialID != "" {
713+
credentialID = req.RegistryCredential.CredentialID
712714
cred, err := s.credentialsManager.GetCredential(req.RegistryCredential.CredentialID)
713715
if err != nil {
714716
registryLoginError = "Failed to load credential: " + err.Error()
@@ -722,7 +724,7 @@ func (s *Server) createDeployment(c *gin.Context) {
722724
password = req.RegistryCredential.Password
723725

724726
if req.RegistryCredential.SaveCredential && req.RegistryCredential.CredentialName != "" {
725-
_, err := s.credentialsManager.CreateCredential(
727+
newCred, err := s.credentialsManager.CreateCredential(
726728
req.RegistryCredential.CredentialName,
727729
"docker-hub",
728730
username,
@@ -732,6 +734,8 @@ func (s *Server) createDeployment(c *gin.Context) {
732734
)
733735
if err != nil {
734736
log.Printf("Warning: failed to save credential: %v", err)
737+
} else {
738+
credentialID = newCred.ID
735739
}
736740
}
737741
}
@@ -744,6 +748,13 @@ func (s *Server) createDeployment(c *gin.Context) {
744748
}
745749
}
746750

751+
if credentialID != "" && req.Metadata != nil {
752+
req.Metadata.CredentialID = credentialID
753+
if err := s.manager.SaveMetadata(req.Name, req.Metadata); err != nil {
754+
log.Printf("Warning: failed to update metadata with credential ID: %v", err)
755+
}
756+
}
757+
747758
var startOutput string
748759
var startError string
749760
if req.AutoStart {
@@ -1167,6 +1178,25 @@ func (s *Server) pullDeploymentImage(c *gin.Context) {
11671178
}
11681179
_ = c.ShouldBindJSON(&req)
11691180

1181+
deployment, err := s.manager.GetDeployment(name)
1182+
if err != nil {
1183+
c.JSON(http.StatusNotFound, gin.H{
1184+
"error": "Deployment not found: " + err.Error(),
1185+
})
1186+
return
1187+
}
1188+
1189+
if deployment.Metadata != nil && deployment.Metadata.CredentialID != "" {
1190+
cred, err := s.credentialsManager.GetCredential(deployment.Metadata.CredentialID)
1191+
if err != nil {
1192+
log.Printf("Warning: failed to load credential %s for pull: %v", deployment.Metadata.CredentialID, err)
1193+
} else {
1194+
if err := credentials.DockerLogin("", cred.Username, cred.Password); err != nil {
1195+
log.Printf("Warning: registry login failed for pull: %v", err)
1196+
}
1197+
}
1198+
}
1199+
11701200
output, err := s.manager.PullDeployment(name, req.OnlyLatest)
11711201
if err != nil {
11721202
c.JSON(http.StatusInternalServerError, gin.H{

pkg/models/deployment.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type ServiceMetadata struct {
3232
QuickActions []QuickAction `yaml:"quick_actions,omitempty" json:"quick_actions,omitempty"`
3333
Security *DeploymentSecurityConfig `yaml:"security,omitempty" json:"security,omitempty"`
3434
Backup *BackupSpec `yaml:"backup,omitempty" json:"backup,omitempty"`
35+
CredentialID string `yaml:"credential_id,omitempty" json:"credential_id,omitempty"`
3536
}
3637

3738
type BackupSpec struct {

0 commit comments

Comments
 (0)