github.com/ddev/ddev@v1.23.2-0.20240519125000-d824ffe36ff3/pkg/ddevapp/ssh_auth.go (about)

     1  package ddevapp
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  	"path"
     8  	"path/filepath"
     9  	"text/template"
    10  
    11  	"github.com/ddev/ddev/pkg/dockerutil"
    12  	"github.com/ddev/ddev/pkg/globalconfig"
    13  	"github.com/ddev/ddev/pkg/util"
    14  	"github.com/ddev/ddev/pkg/versionconstants"
    15  	dockerTypes "github.com/docker/docker/api/types"
    16  )
    17  
    18  // SSHAuthName is the "machine name" of the ddev-ssh-agent docker-compose service
    19  const SSHAuthName = "ddev-ssh-agent"
    20  
    21  // SSHAuthComposeYAMLPath returns the filepath to the base .ssh-auth-compose yaml file.
    22  func SSHAuthComposeYAMLPath() string {
    23  	globalDir := globalconfig.GetGlobalDdevDir()
    24  	dest := path.Join(globalDir, ".ssh-auth-compose.yaml")
    25  	return dest
    26  }
    27  
    28  // FullRenderedSSHAuthComposeYAMLPath returns the filepath to the rendered
    29  // .ssh-auth-compose-full.yaml file.
    30  func FullRenderedSSHAuthComposeYAMLPath() string {
    31  	globalDir := globalconfig.GetGlobalDdevDir()
    32  	dest := path.Join(globalDir, ".ssh-auth-compose-full.yaml")
    33  	return dest
    34  }
    35  
    36  // EnsureSSHAgentContainer ensures the ssh-auth container is running.
    37  func (app *DdevApp) EnsureSSHAgentContainer() error {
    38  	sshContainer, err := findDdevSSHAuth()
    39  	if err != nil {
    40  		return err
    41  	}
    42  	// If we already have a running ssh container, there's nothing to do.
    43  	if sshContainer != nil && (sshContainer.State == "running" || sshContainer.State == "starting") {
    44  		return nil
    45  	}
    46  
    47  	dockerutil.EnsureDdevNetwork()
    48  
    49  	path, err := app.CreateSSHAuthComposeFile()
    50  	if err != nil {
    51  		return err
    52  	}
    53  
    54  	app.DockerEnv()
    55  
    56  	// run docker-compose up -d
    57  	// This will force-recreate, discarding existing auth if there is a stopped container.
    58  	_, _, err = dockerutil.ComposeCmd(&dockerutil.ComposeCmdOpts{
    59  		ComposeFiles: []string{path},
    60  		Action:       []string{"-p", SSHAuthName, "up", "--build", "--force-recreate", "-d"},
    61  	})
    62  	if err != nil {
    63  		return fmt.Errorf("failed to start ddev-ssh-agent: %v", err)
    64  	}
    65  
    66  	// ensure we have a happy sshAuth
    67  	label := map[string]string{"com.docker.compose.project": SSHAuthName}
    68  	sshWaitTimeout := 60
    69  	_, err = dockerutil.ContainerWait(sshWaitTimeout, label)
    70  	if err != nil {
    71  		return fmt.Errorf("ddev-ssh-agent failed to become ready; debug with 'docker logs ddev-ssh-agent' and 'docker inspect --format \"{{json .State.Health }}\" ddev-ssh-agent'; error: %v", err)
    72  	}
    73  
    74  	util.Warning("ssh-agent container is running: If you want to add authentication to the ssh-agent container, run 'ddev auth ssh' to enable your keys.")
    75  	return nil
    76  }
    77  
    78  // RemoveSSHAgentContainer brings down the ddev-ssh-agent if it's running.
    79  func RemoveSSHAgentContainer() error {
    80  	// Stop the container if it exists
    81  	err := dockerutil.RemoveContainer(globalconfig.DdevSSHAgentContainer)
    82  	if err != nil {
    83  		if ok := dockerutil.IsErrNotFound(err); !ok {
    84  			return err
    85  		}
    86  	}
    87  	util.Warning("The ddev-ssh-agent container has been removed. When you start it again you will have to use 'ddev auth ssh' to provide key authentication again.")
    88  	return nil
    89  }
    90  
    91  // CreateSSHAuthComposeFile creates the docker-compose file for the ddev-ssh-agent
    92  func (app *DdevApp) CreateSSHAuthComposeFile() (string, error) {
    93  
    94  	var doc bytes.Buffer
    95  	f, ferr := os.Create(SSHAuthComposeYAMLPath())
    96  	if ferr != nil {
    97  		return "", ferr
    98  	}
    99  	defer util.CheckClose(f)
   100  
   101  	context := "./.sshimageBuild"
   102  	err := WriteBuildDockerfile(filepath.Join(globalconfig.GetGlobalDdevDir(), context, "Dockerfile"), "", nil, "", "")
   103  	if err != nil {
   104  		return "", err
   105  	}
   106  
   107  	uid, gid, username := util.GetContainerUIDGid()
   108  
   109  	app.DockerEnv()
   110  
   111  	templateVars := map[string]interface{}{
   112  		"ssh_auth_image": versionconstants.SSHAuthImage,
   113  		"ssh_auth_tag":   versionconstants.SSHAuthTag,
   114  		"Username":       username,
   115  		"UID":            uid,
   116  		"GID":            gid,
   117  		"BuildContext":   context,
   118  	}
   119  	t, err := template.New("ssh_auth_compose_template.yaml").ParseFS(bundledAssets, "ssh_auth_compose_template.yaml")
   120  	if err != nil {
   121  		return "", err
   122  	}
   123  	err = t.Execute(&doc, templateVars)
   124  	util.CheckErr(err)
   125  	_, err = f.WriteString(doc.String())
   126  	util.CheckErr(err)
   127  
   128  	fullHandle, err := os.Create(FullRenderedSSHAuthComposeYAMLPath())
   129  	if err != nil {
   130  		return "", err
   131  	}
   132  
   133  	userFiles, err := filepath.Glob(filepath.Join(globalconfig.GetGlobalDdevDir(), "ssh-auth-compose.*.yaml"))
   134  	if err != nil {
   135  		return "", err
   136  	}
   137  	files := append([]string{SSHAuthComposeYAMLPath()}, userFiles...)
   138  	fullContents, _, err := dockerutil.ComposeCmd(&dockerutil.ComposeCmdOpts{
   139  		ComposeFiles: files,
   140  		Action:       []string{"config"},
   141  	})
   142  	if err != nil {
   143  		return "", err
   144  	}
   145  	_, err = fullHandle.WriteString(fullContents)
   146  	if err != nil {
   147  		return "", err
   148  	}
   149  	return FullRenderedSSHAuthComposeYAMLPath(), nil
   150  }
   151  
   152  // findDdevSSHAuth uses FindContainerByLabels to get our sshAuth container and
   153  // return it (or nil if it doesn't exist yet)
   154  func findDdevSSHAuth() (*dockerTypes.Container, error) {
   155  	containerQuery := map[string]string{
   156  		"com.docker.compose.project": SSHAuthName,
   157  	}
   158  
   159  	container, err := dockerutil.FindContainerByLabels(containerQuery)
   160  	if err != nil {
   161  		return nil, fmt.Errorf("failed to execute findContainersByLabels, %v", err)
   162  	}
   163  	return container, nil
   164  }
   165  
   166  // RenderSSHAuthStatus returns a user-friendly string showing sshAuth-status
   167  func RenderSSHAuthStatus() string {
   168  	status := GetSSHAuthStatus()
   169  	var renderedStatus string
   170  
   171  	switch status {
   172  	case "healthy":
   173  		renderedStatus = util.ColorizeText(status, "green")
   174  	case "exited":
   175  		fallthrough
   176  	default:
   177  		renderedStatus = util.ColorizeText(status, "red")
   178  	}
   179  	return fmt.Sprintf("\nssh-auth status: %v", renderedStatus)
   180  }
   181  
   182  // GetSSHAuthStatus outputs sshAuth status and warning if not
   183  // running or healthy, as applicable.
   184  func GetSSHAuthStatus() string {
   185  	label := map[string]string{"com.docker.compose.project": SSHAuthName}
   186  	container, err := dockerutil.FindContainerByLabels(label)
   187  
   188  	if err != nil {
   189  		util.Error("Failed to execute FindContainerByLabels(%v): %v", label, err)
   190  		return SiteStopped
   191  	}
   192  	if container == nil {
   193  		return SiteStopped
   194  	}
   195  	health, _ := dockerutil.GetContainerHealth(container)
   196  	return health
   197  
   198  }