github.com/drud/ddev@v1.21.5-alpha1.0.20230226034409-94fcc4b94453/pkg/ddevapp/ssh_auth.go (about)

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