github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/repo/repotest/server.go (about)

     1  /*
     2  Copyright The Helm Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package repotest
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"io/ioutil"
    22  	"net/http"
    23  	"net/http/httptest"
    24  	"os"
    25  	"path/filepath"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/distribution/distribution/v3/configuration"
    30  	"github.com/distribution/distribution/v3/registry"
    31  	_ "github.com/distribution/distribution/v3/registry/auth/htpasswd"           // used for docker test registry
    32  	_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" // used for docker test registry
    33  	"github.com/phayes/freeport"
    34  	"golang.org/x/crypto/bcrypt"
    35  	"sigs.k8s.io/yaml"
    36  
    37  	"github.com/stefanmcshane/helm/internal/tlsutil"
    38  	"github.com/stefanmcshane/helm/pkg/chart"
    39  	"github.com/stefanmcshane/helm/pkg/chart/loader"
    40  	"github.com/stefanmcshane/helm/pkg/chartutil"
    41  	ociRegistry "github.com/stefanmcshane/helm/pkg/registry"
    42  	"github.com/stefanmcshane/helm/pkg/repo"
    43  )
    44  
    45  // NewTempServerWithCleanup creates a server inside of a temp dir.
    46  //
    47  // If the passed in string is not "", it will be treated as a shell glob, and files
    48  // will be copied from that path to the server's docroot.
    49  //
    50  // The caller is responsible for stopping the server.
    51  // The temp dir will be removed by testing package automatically when test finished.
    52  func NewTempServerWithCleanup(t *testing.T, glob string) (*Server, error) {
    53  	srv, err := NewTempServer(glob)
    54  	t.Cleanup(func() { os.RemoveAll(srv.docroot) })
    55  	return srv, err
    56  }
    57  
    58  // Set up a fake repo with basic auth enabled
    59  func NewTempServerWithCleanupAndBasicAuth(t *testing.T, glob string) *Server {
    60  	srv, err := NewTempServerWithCleanup(t, glob)
    61  	srv.Stop()
    62  	if err != nil {
    63  		t.Fatal(err)
    64  	}
    65  	srv.WithMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    66  		username, password, ok := r.BasicAuth()
    67  		if !ok || username != "username" || password != "password" {
    68  			t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password)
    69  		}
    70  	}))
    71  	srv.Start()
    72  	return srv
    73  }
    74  
    75  type OCIServer struct {
    76  	*registry.Registry
    77  	RegistryURL  string
    78  	Dir          string
    79  	TestUsername string
    80  	TestPassword string
    81  	Client       *ociRegistry.Client
    82  }
    83  
    84  type OCIServerRunConfig struct {
    85  	DependingChart *chart.Chart
    86  }
    87  
    88  type OCIServerOpt func(config *OCIServerRunConfig)
    89  
    90  func WithDependingChart(c *chart.Chart) OCIServerOpt {
    91  	return func(config *OCIServerRunConfig) {
    92  		config.DependingChart = c
    93  	}
    94  }
    95  
    96  func NewOCIServer(t *testing.T, dir string) (*OCIServer, error) {
    97  	testHtpasswdFileBasename := "authtest.htpasswd"
    98  	testUsername, testPassword := "username", "password"
    99  
   100  	pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost)
   101  	if err != nil {
   102  		t.Fatal("error generating bcrypt password for test htpasswd file")
   103  	}
   104  	htpasswdPath := filepath.Join(dir, testHtpasswdFileBasename)
   105  	err = ioutil.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644)
   106  	if err != nil {
   107  		t.Fatalf("error creating test htpasswd file")
   108  	}
   109  
   110  	// Registry config
   111  	config := &configuration.Configuration{}
   112  	port, err := freeport.GetFreePort()
   113  	if err != nil {
   114  		t.Fatalf("error finding free port for test registry")
   115  	}
   116  
   117  	config.HTTP.Addr = fmt.Sprintf(":%d", port)
   118  	config.HTTP.DrainTimeout = time.Duration(10) * time.Second
   119  	config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}}
   120  	config.Auth = configuration.Auth{
   121  		"htpasswd": configuration.Parameters{
   122  			"realm": "localhost",
   123  			"path":  htpasswdPath,
   124  		},
   125  	}
   126  
   127  	registryURL := fmt.Sprintf("localhost:%d", port)
   128  
   129  	r, err := registry.NewRegistry(context.Background(), config)
   130  	if err != nil {
   131  		t.Fatal(err)
   132  	}
   133  
   134  	return &OCIServer{
   135  		Registry:     r,
   136  		RegistryURL:  registryURL,
   137  		TestUsername: testUsername,
   138  		TestPassword: testPassword,
   139  		Dir:          dir,
   140  	}, nil
   141  }
   142  
   143  func (srv *OCIServer) Run(t *testing.T, opts ...OCIServerOpt) {
   144  	cfg := &OCIServerRunConfig{}
   145  	for _, fn := range opts {
   146  		fn(cfg)
   147  	}
   148  
   149  	go srv.ListenAndServe()
   150  
   151  	credentialsFile := filepath.Join(srv.Dir, "config.json")
   152  
   153  	// init test client
   154  	registryClient, err := ociRegistry.NewClient(
   155  		ociRegistry.ClientOptDebug(true),
   156  		ociRegistry.ClientOptEnableCache(true),
   157  		ociRegistry.ClientOptWriter(os.Stdout),
   158  		ociRegistry.ClientOptCredentialsFile(credentialsFile),
   159  	)
   160  	if err != nil {
   161  		t.Fatalf("error creating registry client")
   162  	}
   163  
   164  	err = registryClient.Login(
   165  		srv.RegistryURL,
   166  		ociRegistry.LoginOptBasicAuth(srv.TestUsername, srv.TestPassword),
   167  		ociRegistry.LoginOptInsecure(false))
   168  	if err != nil {
   169  		t.Fatalf("error logging into registry with good credentials")
   170  	}
   171  
   172  	ref := fmt.Sprintf("%s/u/ocitestuser/oci-dependent-chart:0.1.0", srv.RegistryURL)
   173  
   174  	err = chartutil.ExpandFile(srv.Dir, filepath.Join(srv.Dir, "oci-dependent-chart-0.1.0.tgz"))
   175  	if err != nil {
   176  		t.Fatal(err)
   177  	}
   178  
   179  	// valid chart
   180  	ch, err := loader.LoadDir(filepath.Join(srv.Dir, "oci-dependent-chart"))
   181  	if err != nil {
   182  		t.Fatal("error loading chart")
   183  	}
   184  
   185  	err = os.RemoveAll(filepath.Join(srv.Dir, "oci-dependent-chart"))
   186  	if err != nil {
   187  		t.Fatal("error removing chart before push")
   188  	}
   189  
   190  	// save it back to disk..
   191  	absPath, err := chartutil.Save(ch, srv.Dir)
   192  	if err != nil {
   193  		t.Fatal("could not create chart archive")
   194  	}
   195  
   196  	// load it into memory...
   197  	contentBytes, err := ioutil.ReadFile(absPath)
   198  	if err != nil {
   199  		t.Fatal("could not load chart into memory")
   200  	}
   201  
   202  	result, err := registryClient.Push(contentBytes, ref)
   203  	if err != nil {
   204  		t.Fatalf("error pushing dependent chart: %s", err)
   205  	}
   206  	t.Logf("Manifest.Digest: %s, Manifest.Size: %d, "+
   207  		"Config.Digest: %s, Config.Size: %d, "+
   208  		"Chart.Digest: %s, Chart.Size: %d",
   209  		result.Manifest.Digest, result.Manifest.Size,
   210  		result.Config.Digest, result.Config.Size,
   211  		result.Chart.Digest, result.Chart.Size)
   212  
   213  	srv.Client = registryClient
   214  	c := cfg.DependingChart
   215  	if c == nil {
   216  		return
   217  	}
   218  
   219  	dependingRef := fmt.Sprintf("%s/u/ocitestuser/%s:%s",
   220  		srv.RegistryURL, c.Metadata.Name, c.Metadata.Version)
   221  
   222  	// load it into memory...
   223  	absPath = filepath.Join(srv.Dir,
   224  		fmt.Sprintf("%s-%s.tgz", c.Metadata.Name, c.Metadata.Version))
   225  	contentBytes, err = ioutil.ReadFile(absPath)
   226  	if err != nil {
   227  		t.Fatal("could not load chart into memory")
   228  	}
   229  
   230  	result, err = registryClient.Push(contentBytes, dependingRef)
   231  	if err != nil {
   232  		t.Fatalf("error pushing depending chart: %s", err)
   233  	}
   234  	t.Logf("Manifest.Digest: %s, Manifest.Size: %d, "+
   235  		"Config.Digest: %s, Config.Size: %d, "+
   236  		"Chart.Digest: %s, Chart.Size: %d",
   237  		result.Manifest.Digest, result.Manifest.Size,
   238  		result.Config.Digest, result.Config.Size,
   239  		result.Chart.Digest, result.Chart.Size)
   240  }
   241  
   242  // NewTempServer creates a server inside of a temp dir.
   243  //
   244  // If the passed in string is not "", it will be treated as a shell glob, and files
   245  // will be copied from that path to the server's docroot.
   246  //
   247  // The caller is responsible for destroying the temp directory as well as stopping
   248  // the server.
   249  //
   250  // Deprecated: use NewTempServerWithCleanup
   251  func NewTempServer(glob string) (*Server, error) {
   252  	tdir, err := ioutil.TempDir("", "helm-repotest-")
   253  	if err != nil {
   254  		return nil, err
   255  	}
   256  	srv := NewServer(tdir)
   257  
   258  	if glob != "" {
   259  		if _, err := srv.CopyCharts(glob); err != nil {
   260  			srv.Stop()
   261  			return srv, err
   262  		}
   263  	}
   264  
   265  	return srv, nil
   266  }
   267  
   268  // NewServer creates a repository server for testing.
   269  //
   270  // docroot should be a temp dir managed by the caller.
   271  //
   272  // This will start the server, serving files off of the docroot.
   273  //
   274  // Use CopyCharts to move charts into the repository and then index them
   275  // for service.
   276  func NewServer(docroot string) *Server {
   277  	root, err := filepath.Abs(docroot)
   278  	if err != nil {
   279  		panic(err)
   280  	}
   281  	srv := &Server{
   282  		docroot: root,
   283  	}
   284  	srv.Start()
   285  	// Add the testing repository as the only repo.
   286  	if err := setTestingRepository(srv.URL(), filepath.Join(root, "repositories.yaml")); err != nil {
   287  		panic(err)
   288  	}
   289  	return srv
   290  }
   291  
   292  // Server is an implementation of a repository server for testing.
   293  type Server struct {
   294  	docroot    string
   295  	srv        *httptest.Server
   296  	middleware http.HandlerFunc
   297  }
   298  
   299  // WithMiddleware injects middleware in front of the server. This can be used to inject
   300  // additional functionality like layering in an authentication frontend.
   301  func (s *Server) WithMiddleware(middleware http.HandlerFunc) {
   302  	s.middleware = middleware
   303  }
   304  
   305  // Root gets the docroot for the server.
   306  func (s *Server) Root() string {
   307  	return s.docroot
   308  }
   309  
   310  // CopyCharts takes a glob expression and copies those charts to the server root.
   311  func (s *Server) CopyCharts(origin string) ([]string, error) {
   312  	files, err := filepath.Glob(origin)
   313  	if err != nil {
   314  		return []string{}, err
   315  	}
   316  	copied := make([]string, len(files))
   317  	for i, f := range files {
   318  		base := filepath.Base(f)
   319  		newname := filepath.Join(s.docroot, base)
   320  		data, err := ioutil.ReadFile(f)
   321  		if err != nil {
   322  			return []string{}, err
   323  		}
   324  		if err := ioutil.WriteFile(newname, data, 0644); err != nil {
   325  			return []string{}, err
   326  		}
   327  		copied[i] = newname
   328  	}
   329  
   330  	err = s.CreateIndex()
   331  	return copied, err
   332  }
   333  
   334  // CreateIndex will read docroot and generate an index.yaml file.
   335  func (s *Server) CreateIndex() error {
   336  	// generate the index
   337  	index, err := repo.IndexDirectory(s.docroot, s.URL())
   338  	if err != nil {
   339  		return err
   340  	}
   341  
   342  	d, err := yaml.Marshal(index)
   343  	if err != nil {
   344  		return err
   345  	}
   346  
   347  	ifile := filepath.Join(s.docroot, "index.yaml")
   348  	return ioutil.WriteFile(ifile, d, 0644)
   349  }
   350  
   351  func (s *Server) Start() {
   352  	s.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   353  		if s.middleware != nil {
   354  			s.middleware.ServeHTTP(w, r)
   355  		}
   356  		http.FileServer(http.Dir(s.docroot)).ServeHTTP(w, r)
   357  	}))
   358  }
   359  
   360  func (s *Server) StartTLS() {
   361  	cd := "../../testdata"
   362  	ca, pub, priv := filepath.Join(cd, "rootca.crt"), filepath.Join(cd, "crt.pem"), filepath.Join(cd, "key.pem")
   363  
   364  	s.srv = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   365  		if s.middleware != nil {
   366  			s.middleware.ServeHTTP(w, r)
   367  		}
   368  		http.FileServer(http.Dir(s.Root())).ServeHTTP(w, r)
   369  	}))
   370  	tlsConf, err := tlsutil.NewClientTLS(pub, priv, ca)
   371  	if err != nil {
   372  		panic(err)
   373  	}
   374  	tlsConf.ServerName = "helm.sh"
   375  	s.srv.TLS = tlsConf
   376  	s.srv.StartTLS()
   377  
   378  	// Set up repositories config with ca file
   379  	repoConfig := filepath.Join(s.Root(), "repositories.yaml")
   380  
   381  	r := repo.NewFile()
   382  	r.Add(&repo.Entry{
   383  		Name:   "test",
   384  		URL:    s.URL(),
   385  		CAFile: filepath.Join("../../testdata", "rootca.crt"),
   386  	})
   387  
   388  	if err := r.WriteFile(repoConfig, 0644); err != nil {
   389  		panic(err)
   390  	}
   391  }
   392  
   393  // Stop stops the server and closes all connections.
   394  //
   395  // It should be called explicitly.
   396  func (s *Server) Stop() {
   397  	s.srv.Close()
   398  }
   399  
   400  // URL returns the URL of the server.
   401  //
   402  // Example:
   403  //	http://localhost:1776
   404  func (s *Server) URL() string {
   405  	return s.srv.URL
   406  }
   407  
   408  // LinkIndices links the index created with CreateIndex and makes a symbolic link to the cache index.
   409  //
   410  // This makes it possible to simulate a local cache of a repository.
   411  func (s *Server) LinkIndices() error {
   412  	lstart := filepath.Join(s.docroot, "index.yaml")
   413  	ldest := filepath.Join(s.docroot, "test-index.yaml")
   414  	return os.Symlink(lstart, ldest)
   415  }
   416  
   417  // setTestingRepository sets up a testing repository.yaml with only the given URL.
   418  func setTestingRepository(url, fname string) error {
   419  	r := repo.NewFile()
   420  	r.Add(&repo.Entry{
   421  		Name: "test",
   422  		URL:  url,
   423  	})
   424  	return r.WriteFile(fname, 0644)
   425  }