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 }