go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gce/appengine/config/config.go (about)

     1  // Copyright 2018 The LUCI Authors.
     2  //
     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  // Package config knows how import configs from LUCI-config.
    16  package config
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"net/http"
    22  	"strings"
    23  
    24  	"github.com/golang/protobuf/proto"
    25  
    26  	"go.chromium.org/luci/common/errors"
    27  	"go.chromium.org/luci/common/logging"
    28  	"go.chromium.org/luci/config"
    29  	"go.chromium.org/luci/config/cfgclient"
    30  	"go.chromium.org/luci/config/validation"
    31  	"go.chromium.org/luci/server/router"
    32  
    33  	gce "go.chromium.org/luci/gce/api/config/v1"
    34  	"go.chromium.org/luci/gce/api/projects/v1"
    35  	"go.chromium.org/luci/gce/appengine/rpc"
    36  )
    37  
    38  // projectsFile is the name of the projects config file.
    39  const projectsFile = "projects.cfg"
    40  
    41  // vmsFile is the name of the VMs config file.
    42  const vmsFile = "vms.cfg"
    43  
    44  // Config encapsulates the service config.
    45  type Config struct {
    46  	revision string
    47  	Projects *projects.Configs
    48  	VMs      *gce.Configs
    49  }
    50  
    51  // prjKey is the key to a projects.ProjectsServer in the context.
    52  var prjKey = "prj"
    53  
    54  // withProjServer returns a new context with the given projects.ProjectsServer
    55  // installed.
    56  func withProjServer(c context.Context, srv projects.ProjectsServer) context.Context {
    57  	return context.WithValue(c, &prjKey, srv)
    58  }
    59  
    60  // getProjServer returns the projects.ProjectsServer installed in the current
    61  // context.
    62  func getProjServer(c context.Context) projects.ProjectsServer {
    63  	return c.Value(&prjKey).(projects.ProjectsServer)
    64  }
    65  
    66  // vmsKey is the key to a gce.ConfigurationServer in the context.
    67  var vmsKey = "vms"
    68  
    69  // withVMsServer returns a new context with the given gce.ConfigurationServer
    70  // installed.
    71  func withVMsServer(c context.Context, srv gce.ConfigurationServer) context.Context {
    72  	return context.WithValue(c, &vmsKey, srv)
    73  }
    74  
    75  // getVMsServer returns the gce.ConfigurationServer installed in the current
    76  // context.
    77  func getVMsServer(c context.Context) gce.ConfigurationServer {
    78  	return c.Value(&vmsKey).(gce.ConfigurationServer)
    79  }
    80  
    81  // fetch fetches configs from the config service.
    82  func fetch(c context.Context) (*Config, error) {
    83  	cli := cfgclient.Client(c)
    84  	rev := ""
    85  	vms := &gce.Configs{}
    86  	switch vmsCfg, err := cli.GetConfig(c, "services/${appid}", vmsFile, false); {
    87  	case err == config.ErrNoConfig:
    88  		logging.Debugf(c, "%q not found", vmsFile)
    89  	case err != nil:
    90  		return nil, errors.Annotate(err, "failed to fetch %q", vmsFile).Err()
    91  	default:
    92  		rev = vmsCfg.Revision
    93  		logging.Debugf(c, "found %q revision %s", vmsFile, vmsCfg.Revision)
    94  		if err := proto.UnmarshalText(vmsCfg.Content, vms); err != nil {
    95  			return nil, errors.Annotate(err, "failed to load %q", vmsFile).Err()
    96  		}
    97  	}
    98  	prjs := &projects.Configs{}
    99  	switch prjsCfg, err := cli.GetConfig(c, "services/${appid}", projectsFile, false); {
   100  	case err == config.ErrNoConfig:
   101  		logging.Debugf(c, "%q not found", projectsFile)
   102  	case err != nil:
   103  		return nil, errors.Annotate(err, "failed to fetch %q", projectsFile).Err()
   104  	default:
   105  		logging.Debugf(c, "found %q revision %s", projectsFile, prjsCfg.Revision)
   106  		if rev != "" && prjsCfg.Revision != rev {
   107  			return nil, errors.Reason("config revision mismatch").Err()
   108  		}
   109  		if err := proto.UnmarshalText(prjsCfg.Content, prjs); err != nil {
   110  			return nil, errors.Annotate(err, "failed to load %q", projectsFile).Err()
   111  		}
   112  	}
   113  	return &Config{
   114  		revision: rev,
   115  		Projects: prjs,
   116  		VMs:      vms,
   117  	}, nil
   118  }
   119  
   120  // validate validates configs.
   121  func validate(c context.Context, cfg *Config) error {
   122  	v := &validation.Context{Context: c}
   123  	v.SetFile(projectsFile)
   124  	cfg.Projects.Validate(v)
   125  	v.SetFile(vmsFile)
   126  	cfg.VMs.Validate(v)
   127  	return v.Finalize()
   128  }
   129  
   130  // deref dereferences VMs metadata by fetching referenced files.
   131  func deref(c context.Context, cfg *Config) error {
   132  	// Cache fetched files.
   133  	fileMap := make(map[string]string)
   134  	cli := cfgclient.Client(c)
   135  	for _, v := range cfg.VMs.GetVms() {
   136  		for i, m := range v.GetAttributes().Metadata {
   137  			if m.GetFromFile() != "" {
   138  				parts := strings.SplitN(m.GetFromFile(), ":", 2)
   139  				if len(parts) < 2 {
   140  					return errors.Reason("metadata from file must be in key:value form").Err()
   141  				}
   142  				file := parts[1]
   143  				if _, ok := fileMap[file]; !ok {
   144  					fileCfg, err := cli.GetConfig(c, "services/${appid}", file, false)
   145  					if err != nil {
   146  						return errors.Annotate(err, "failed to fetch %q", file).Err()
   147  					}
   148  					logging.Debugf(c, "found %q revision %s", file, fileCfg.Revision)
   149  					if fileCfg.Revision != cfg.revision {
   150  						return errors.Reason("config revision mismatch %q", fileCfg.Revision).Err()
   151  					}
   152  					fileMap[file] = fileCfg.Content
   153  				}
   154  				// fileMap[file] definitely exists.
   155  				key := parts[0]
   156  				val := fileMap[file]
   157  				v.Attributes.Metadata[i].Metadata = &gce.Metadata_FromText{
   158  					FromText: fmt.Sprintf("%s:%s", key, val),
   159  				}
   160  			}
   161  		}
   162  	}
   163  	return nil
   164  }
   165  
   166  // normalize normalizes VMs durations by converting them to seconds, and sets
   167  // output-only properties.
   168  func normalize(c context.Context, cfg *Config) error {
   169  	for _, p := range cfg.Projects.GetProject() {
   170  		p.Revision = cfg.revision
   171  	}
   172  	for _, v := range cfg.VMs.GetVms() {
   173  		for _, ch := range v.Amount.GetChange() {
   174  			if err := ch.Length.Normalize(); err != nil {
   175  				return errors.Annotate(err, "failed to normalize %q", v.Prefix).Err()
   176  			}
   177  		}
   178  		if err := v.Lifetime.Normalize(); err != nil {
   179  			return errors.Annotate(err, "failed to normalize %q", v.Prefix).Err()
   180  		}
   181  		v.Revision = cfg.revision
   182  		if err := v.Timeout.Normalize(); err != nil {
   183  			return errors.Annotate(err, "failed to normalize %q", v.Prefix).Err()
   184  		}
   185  	}
   186  	return nil
   187  }
   188  
   189  // syncVMs synchronizes the given validated VM configs.
   190  func syncVMs(c context.Context, vms []*gce.Config) error {
   191  	// Fetch existing configs.
   192  	srv := getVMsServer(c)
   193  	rsp, err := srv.List(c, &gce.ListRequest{})
   194  	if err != nil {
   195  		return errors.Annotate(err, "failed to fetch VMs configs").Err()
   196  	}
   197  	// Track the revision of each config.
   198  	revs := make(map[string]string, len(rsp.Configs))
   199  	for _, v := range rsp.Configs {
   200  		revs[v.Prefix] = v.Revision
   201  	}
   202  	logging.Debugf(c, "fetched %d VMs configs", len(rsp.Configs))
   203  
   204  	// Update configs to new revisions.
   205  	ens := &gce.EnsureRequest{}
   206  	for _, v := range vms {
   207  		rev, ok := revs[v.Prefix]
   208  		delete(revs, v.Prefix)
   209  		if ok && rev == v.Revision {
   210  			continue
   211  		}
   212  		ens.Id = v.Prefix
   213  		ens.Config = v
   214  		if _, err := srv.Ensure(c, ens); err != nil {
   215  			return errors.Annotate(err, "failed to ensure VMs config %q", ens.Id).Err()
   216  		}
   217  	}
   218  
   219  	// Delete unreferenced configs.
   220  	del := &gce.DeleteRequest{}
   221  	for id := range revs {
   222  		del.Id = id
   223  		if _, err := srv.Delete(c, del); err != nil {
   224  			return errors.Annotate(err, "failed to delete VMs config %q", del.Id).Err()
   225  		}
   226  		logging.Debugf(c, "deleted VMs config %q", del.Id)
   227  	}
   228  	return nil
   229  }
   230  
   231  // syncPrjs synchronizes the given validated project configs.
   232  func syncPrjs(c context.Context, prjs []*projects.Config) error {
   233  	// Fetch existing configs.
   234  	srv := getProjServer(c)
   235  	rsp, err := srv.List(c, &projects.ListRequest{})
   236  	if err != nil {
   237  		return errors.Annotate(err, "failed to fetch project configs").Err()
   238  	}
   239  	// Track the revision of each config.
   240  	revs := make(map[string]string, len(rsp.Projects))
   241  	for _, p := range rsp.Projects {
   242  		revs[p.Project] = p.Revision
   243  	}
   244  	logging.Debugf(c, "fetched %d project configs", len(rsp.Projects))
   245  
   246  	// Update configs to new revisions.
   247  	ens := &projects.EnsureRequest{}
   248  	for _, p := range prjs {
   249  		rev, ok := revs[p.Project]
   250  		delete(revs, p.Project)
   251  		if ok && rev == p.Revision {
   252  			continue
   253  		}
   254  		ens.Id = p.Project
   255  		ens.Project = p
   256  		if _, err := srv.Ensure(c, ens); err != nil {
   257  			return errors.Annotate(err, "failed to ensure project config %q", ens.Id).Err()
   258  		}
   259  	}
   260  
   261  	// Delete unreferenced configs.
   262  	del := &projects.DeleteRequest{}
   263  	for id := range revs {
   264  		del.Id = id
   265  		if _, err := srv.Delete(c, del); err != nil {
   266  			return errors.Annotate(err, "failed to delete project config %q", del.Id).Err()
   267  		}
   268  		logging.Debugf(c, "deleted project config %q", del.Id)
   269  	}
   270  	return nil
   271  }
   272  
   273  // sync synchronizes the given validated configs.
   274  func sync(c context.Context, cfg *Config) error {
   275  	if err := syncVMs(c, cfg.VMs.GetVms()); err != nil {
   276  		return errors.Annotate(err, "failed to sync VMs configs").Err()
   277  	}
   278  	if err := syncPrjs(c, cfg.Projects.GetProject()); err != nil {
   279  		return errors.Annotate(err, "failed to sync project configs").Err()
   280  	}
   281  	return nil
   282  }
   283  
   284  // doImport fetches and validates configs from the config service.
   285  // doImport could have been named "import" but it's a reserved keyword.
   286  func doImport(c context.Context) error {
   287  	cfg, err := fetch(c)
   288  	if err != nil {
   289  		return errors.Annotate(err, "failed to fetch configs").Err()
   290  	}
   291  
   292  	// Deref before validating. VMs may be invalid until metadata from file is imported.
   293  	if err := deref(c, cfg); err != nil {
   294  		return errors.Annotate(err, "failed to dereference files").Err()
   295  	}
   296  
   297  	if err := validate(c, cfg); err != nil {
   298  		return errors.Annotate(err, "invalid configs").Err()
   299  	}
   300  
   301  	if err := normalize(c, cfg); err != nil {
   302  		return errors.Annotate(err, "failed to normalize configs").Err()
   303  	}
   304  
   305  	if err := sync(c, cfg); err != nil {
   306  		return errors.Annotate(err, "failed to synchronize configs").Err()
   307  	}
   308  	return nil
   309  }
   310  
   311  // importHandler imports the config from the config service.
   312  func importHandler(c *router.Context) {
   313  	c.Writer.Header().Set("Content-Type", "text/plain")
   314  
   315  	if err := doImport(c.Request.Context()); err != nil {
   316  		errors.Log(c.Request.Context(), err)
   317  		c.Writer.WriteHeader(http.StatusInternalServerError)
   318  		return
   319  	}
   320  
   321  	c.Writer.WriteHeader(http.StatusOK)
   322  }
   323  
   324  // InstallHandlers installs HTTP request handlers into the given router.
   325  func InstallHandlers(r *router.Router, mw router.MiddlewareChain) {
   326  	mw = mw.Extend(func(c *router.Context, next router.Handler) {
   327  		// Install the services.
   328  		c.Request = c.Request.WithContext(withProjServer(c.Request.Context(), &rpc.Projects{}))
   329  		c.Request = c.Request.WithContext(withVMsServer(c.Request.Context(), &rpc.Config{}))
   330  		next(c)
   331  	})
   332  	r.GET("/internal/cron/import-config", mw, importHandler)
   333  }