Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions internal/api/credential_tracking_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package api

import (
"encoding/json"
"strings"
"testing"

"github.com/flatrun/agent/pkg/models"
"gopkg.in/yaml.v3"
)

func TestServiceMetadataCredentialID(t *testing.T) {
metadata := &models.ServiceMetadata{
Name: "test-deployment",
Type: "custom",
Networking: models.NetworkingConfig{
Expose: true,
Domain: "test.example.com",
ContainerPort: 8080,
},
CredentialID: "cred-123",
}

if metadata.CredentialID != "cred-123" {
t.Errorf("expected CredentialID 'cred-123', got '%s'", metadata.CredentialID)
}
}

func TestServiceMetadataCredentialIDEmpty(t *testing.T) {
metadata := &models.ServiceMetadata{
Name: "public-deployment",
Type: "custom",
}

if metadata.CredentialID != "" {
t.Errorf("expected empty CredentialID for public deployment, got '%s'", metadata.CredentialID)
}
}

func TestServiceMetadataCredentialIDOmitempty(t *testing.T) {
metadata := &models.ServiceMetadata{
Name: "test",
Type: "custom",
}

if metadata.CredentialID != "" {
t.Error("CredentialID should be empty by default")
}

metadata.CredentialID = "new-cred-id"
if metadata.CredentialID != "new-cred-id" {
t.Errorf("expected CredentialID 'new-cred-id', got '%s'", metadata.CredentialID)
}
}

func TestServiceMetadataCredentialIDYAMLSerialization(t *testing.T) {
metadata := &models.ServiceMetadata{
Name: "private-app",
Type: "custom",
CredentialID: "cred-abc-123",
}

data, err := yaml.Marshal(metadata)
if err != nil {
t.Fatalf("failed to marshal metadata: %v", err)
}

yamlStr := string(data)
if !strings.Contains(yamlStr, "credential_id: cred-abc-123") {
t.Errorf("YAML should contain credential_id field, got:\n%s", yamlStr)
}

var unmarshaled models.ServiceMetadata
if err := yaml.Unmarshal(data, &unmarshaled); err != nil {
t.Fatalf("failed to unmarshal metadata: %v", err)
}

if unmarshaled.CredentialID != "cred-abc-123" {
t.Errorf("expected CredentialID 'cred-abc-123' after unmarshal, got '%s'", unmarshaled.CredentialID)
}
}

func TestServiceMetadataCredentialIDYAMLOmitsEmpty(t *testing.T) {
metadata := &models.ServiceMetadata{
Name: "public-app",
Type: "custom",
}

data, err := yaml.Marshal(metadata)
if err != nil {
t.Fatalf("failed to marshal metadata: %v", err)
}

yamlStr := string(data)
if strings.Contains(yamlStr, "credential_id") {
t.Errorf("YAML should omit credential_id when empty, got:\n%s", yamlStr)
}
}

func TestServiceMetadataCredentialIDJSONSerialization(t *testing.T) {
metadata := &models.ServiceMetadata{
Name: "private-app",
Type: "custom",
CredentialID: "cred-xyz-789",
}

data, err := json.Marshal(metadata)
if err != nil {
t.Fatalf("failed to marshal metadata: %v", err)
}

jsonStr := string(data)
if !strings.Contains(jsonStr, `"credential_id":"cred-xyz-789"`) {
t.Errorf("JSON should contain credential_id field, got:\n%s", jsonStr)
}

var unmarshaled models.ServiceMetadata
if err := json.Unmarshal(data, &unmarshaled); err != nil {
t.Fatalf("failed to unmarshal metadata: %v", err)
}

if unmarshaled.CredentialID != "cred-xyz-789" {
t.Errorf("expected CredentialID 'cred-xyz-789' after unmarshal, got '%s'", unmarshaled.CredentialID)
}
}

func TestServiceMetadataCredentialIDJSONOmitsEmpty(t *testing.T) {
metadata := &models.ServiceMetadata{
Name: "public-app",
Type: "custom",
}

data, err := json.Marshal(metadata)
if err != nil {
t.Fatalf("failed to marshal metadata: %v", err)
}

jsonStr := string(data)
if strings.Contains(jsonStr, "credential_id") {
t.Errorf("JSON should omit credential_id when empty, got:\n%s", jsonStr)
}
}
36 changes: 33 additions & 3 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ import (
"github.com/flatrun/agent/internal/audit"
"github.com/flatrun/agent/internal/auth"
"github.com/flatrun/agent/internal/backup"
"github.com/flatrun/agent/internal/dns"
dnsPlugins "github.com/flatrun/agent/pkg/plugins/dns"
"github.com/flatrun/agent/internal/certs"
"github.com/flatrun/agent/internal/credentials"
"github.com/flatrun/agent/internal/database"
"github.com/flatrun/agent/internal/dns"
"github.com/flatrun/agent/internal/docker"
"github.com/flatrun/agent/internal/files"
"github.com/flatrun/agent/internal/infra"
Expand All @@ -36,6 +35,7 @@ import (
"github.com/flatrun/agent/pkg/config"
"github.com/flatrun/agent/pkg/models"
"github.com/flatrun/agent/pkg/plugins"
dnsPlugins "github.com/flatrun/agent/pkg/plugins/dns"
"github.com/flatrun/agent/pkg/subdomain"
"github.com/flatrun/agent/pkg/version"
"github.com/flatrun/agent/templates"
Expand Down Expand Up @@ -705,10 +705,12 @@ func (s *Server) createDeployment(c *gin.Context) {
}

var registryLoginError string
var credentialID string
if req.RegistryCredential != nil {
var username, password string

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

if req.RegistryCredential.SaveCredential && req.RegistryCredential.CredentialName != "" {
_, err := s.credentialsManager.CreateCredential(
newCred, err := s.credentialsManager.CreateCredential(
req.RegistryCredential.CredentialName,
"docker-hub",
username,
Expand All @@ -732,6 +734,8 @@ func (s *Server) createDeployment(c *gin.Context) {
)
if err != nil {
log.Printf("Warning: failed to save credential: %v", err)
} else {
credentialID = newCred.ID
}
}
}
Expand All @@ -744,6 +748,13 @@ func (s *Server) createDeployment(c *gin.Context) {
}
}

if credentialID != "" && req.Metadata != nil {
req.Metadata.CredentialID = credentialID
if err := s.manager.SaveMetadata(req.Name, req.Metadata); err != nil {
log.Printf("Warning: failed to update metadata with credential ID: %v", err)
}
}

var startOutput string
var startError string
if req.AutoStart {
Expand Down Expand Up @@ -1167,6 +1178,25 @@ func (s *Server) pullDeploymentImage(c *gin.Context) {
}
_ = c.ShouldBindJSON(&req)

deployment, err := s.manager.GetDeployment(name)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Deployment not found: " + err.Error(),
})
return
}

if deployment.Metadata != nil && deployment.Metadata.CredentialID != "" {
cred, err := s.credentialsManager.GetCredential(deployment.Metadata.CredentialID)
if err != nil {
log.Printf("Warning: failed to load credential %s for pull: %v", deployment.Metadata.CredentialID, err)
} else {
if err := credentials.DockerLogin("", cred.Username, cred.Password); err != nil {
log.Printf("Warning: registry login failed for pull: %v", err)
}
}
}

output, err := s.manager.PullDeployment(name, req.OnlyLatest)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
Expand Down
1 change: 1 addition & 0 deletions pkg/models/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type ServiceMetadata struct {
QuickActions []QuickAction `yaml:"quick_actions,omitempty" json:"quick_actions,omitempty"`
Security *DeploymentSecurityConfig `yaml:"security,omitempty" json:"security,omitempty"`
Backup *BackupSpec `yaml:"backup,omitempty" json:"backup,omitempty"`
CredentialID string `yaml:"credential_id,omitempty" json:"credential_id,omitempty"`
}

type BackupSpec struct {
Expand Down
Loading