go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config_service/internal/service/update_test.go (about)

     1  // Copyright 2023 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 service
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"net/http"
    21  	"net/http/httptest"
    22  	"testing"
    23  	"time"
    24  
    25  	"google.golang.org/protobuf/encoding/protojson"
    26  	"google.golang.org/protobuf/proto"
    27  	"google.golang.org/protobuf/types/known/emptypb"
    28  
    29  	"go.chromium.org/luci/common/clock"
    30  	"go.chromium.org/luci/common/clock/testclock"
    31  	cfgcommonpb "go.chromium.org/luci/common/proto/config"
    32  	"go.chromium.org/luci/common/testing/prpctest"
    33  	"go.chromium.org/luci/config"
    34  	"go.chromium.org/luci/config/validation"
    35  	"go.chromium.org/luci/gae/service/datastore"
    36  	"go.chromium.org/luci/server/auth/authtest"
    37  
    38  	"go.chromium.org/luci/config_service/internal/common"
    39  	"go.chromium.org/luci/config_service/internal/model"
    40  	"go.chromium.org/luci/config_service/testutil"
    41  
    42  	. "github.com/smartystreets/goconvey/convey"
    43  	. "go.chromium.org/luci/common/testing/assertions"
    44  )
    45  
    46  type testConsumerServer struct {
    47  	cfgcommonpb.UnimplementedConsumerServer
    48  	sm *cfgcommonpb.ServiceMetadata
    49  }
    50  
    51  func (srv *testConsumerServer) GetMetadata(context.Context, *emptypb.Empty) (*cfgcommonpb.ServiceMetadata, error) {
    52  	return srv.sm, nil
    53  }
    54  
    55  func TestUpdateService(t *testing.T) {
    56  	t.Parallel()
    57  
    58  	Convey("Update Service", t, func() {
    59  		ctx := testutil.SetupContext()
    60  		ctx = authtest.MockAuthConfig(ctx)
    61  
    62  		ts := &prpctest.Server{}
    63  		srv := &testConsumerServer{}
    64  		cfgcommonpb.RegisterConsumerServer(ts, srv)
    65  		ts.Start(ctx)
    66  		defer ts.Close()
    67  
    68  		const serviceName = "my-service"
    69  		serviceMetadata := &cfgcommonpb.ServiceMetadata{
    70  			ConfigPatterns: []*cfgcommonpb.ConfigPattern{
    71  				{ConfigSet: string(config.MustProjectSet("foo")), Path: "exact:bar.cfg"},
    72  			},
    73  		}
    74  		srv.sm = serviceMetadata
    75  		serviceInfo := &cfgcommonpb.Service{
    76  			Id:       serviceName,
    77  			Hostname: ts.Host,
    78  		}
    79  		testutil.InjectSelfConfigs(ctx, map[string]proto.Message{
    80  			common.ServiceRegistryFilePath: &cfgcommonpb.ServicesCfg{
    81  				Services: []*cfgcommonpb.Service{
    82  					serviceInfo,
    83  				},
    84  			},
    85  		})
    86  
    87  		Convey("First time update", func() {
    88  			service := &model.Service{
    89  				Name: serviceName,
    90  			}
    91  			So(datastore.Get(ctx, service), ShouldErrLike, datastore.ErrNoSuchEntity)
    92  			So(Update(ctx), ShouldBeNil)
    93  			So(datastore.Get(ctx, service), ShouldBeNil)
    94  			So(service.Info, ShouldResembleProto, serviceInfo)
    95  			So(service.Metadata, ShouldResembleProto, serviceMetadata)
    96  			So(service.UpdateTime, ShouldEqual, clock.Now(ctx).UTC())
    97  		})
    98  
    99  		Convey("Update existing", func() {
   100  			So(Update(ctx), ShouldBeNil)
   101  			Convey("Info changed", func() {
   102  				updatedInfo := proto.Clone(serviceInfo).(*cfgcommonpb.Service)
   103  				updatedInfo.Owners = append(updatedInfo.Owners, "new-owner@example.com")
   104  				testutil.InjectSelfConfigs(ctx, map[string]proto.Message{
   105  					common.ServiceRegistryFilePath: &cfgcommonpb.ServicesCfg{
   106  						Services: []*cfgcommonpb.Service{
   107  							updatedInfo,
   108  						},
   109  					},
   110  				})
   111  				So(Update(ctx), ShouldBeNil)
   112  				service := &model.Service{
   113  					Name: serviceName,
   114  				}
   115  				So(datastore.Get(ctx, service), ShouldBeNil)
   116  				So(service.Info, ShouldResembleProto, updatedInfo)
   117  				So(service.Metadata, ShouldResembleProto, serviceMetadata)
   118  				So(service.UpdateTime, ShouldEqual, clock.Now(ctx).UTC())
   119  			})
   120  			Convey("Metadata changed", func() {
   121  				updated := &cfgcommonpb.ServiceMetadata{
   122  					ConfigPatterns: []*cfgcommonpb.ConfigPattern{
   123  						{ConfigSet: string(config.MustProjectSet("foo")), Path: "exact:bar.cfg"},
   124  						{ConfigSet: string(config.MustProjectSet("foo")), Path: "exact:baz.cfg"},
   125  					},
   126  				}
   127  				srv.sm = updated
   128  				So(Update(ctx), ShouldBeNil)
   129  				service := &model.Service{
   130  					Name: serviceName,
   131  				}
   132  				So(datastore.Get(ctx, service), ShouldBeNil)
   133  				So(service.Info, ShouldResembleProto, serviceInfo)
   134  				So(service.Metadata, ShouldResembleProto, updated)
   135  				So(service.UpdateTime, ShouldEqual, clock.Now(ctx).UTC())
   136  			})
   137  		})
   138  
   139  		Convey("Error for invalid metadata", func() {
   140  			srv.sm = &cfgcommonpb.ServiceMetadata{
   141  				ConfigPatterns: []*cfgcommonpb.ConfigPattern{
   142  					{ConfigSet: string(config.MustProjectSet("foo")), Path: "regex:["},
   143  				},
   144  			}
   145  			So(Update(ctx), ShouldErrLike, "invalid metadata for service")
   146  		})
   147  
   148  		Convey("Skip update if nothing changed", func() {
   149  			So(Update(ctx), ShouldBeNil)
   150  			service := &model.Service{
   151  				Name: serviceName,
   152  			}
   153  			So(datastore.Get(ctx, service), ShouldBeNil)
   154  			prevUpdateTime := service.UpdateTime
   155  			tc := clock.Get(ctx).(testclock.TestClock)
   156  			tc.Add(1 * time.Hour)
   157  			So(Update(ctx), ShouldBeNil)
   158  			service = &model.Service{
   159  				Name: serviceName,
   160  			}
   161  			So(datastore.Get(ctx, service), ShouldBeNil)
   162  			So(service.UpdateTime, ShouldEqual, prevUpdateTime)
   163  		})
   164  
   165  		Convey("Update Service without metadata", func() {
   166  			testutil.InjectSelfConfigs(ctx, map[string]proto.Message{
   167  				common.ServiceRegistryFilePath: &cfgcommonpb.ServicesCfg{
   168  					Services: []*cfgcommonpb.Service{
   169  						{
   170  							Id: serviceName,
   171  						},
   172  					},
   173  				},
   174  			})
   175  			So(Update(ctx), ShouldBeNil)
   176  			service := &model.Service{
   177  				Name: serviceName,
   178  			}
   179  			So(datastore.Get(ctx, service), ShouldBeNil)
   180  			So(service.Info, ShouldResembleProto, &cfgcommonpb.Service{
   181  				Id: serviceName,
   182  			})
   183  			So(service.Metadata, ShouldBeNil)
   184  			So(service.UpdateTime, ShouldEqual, clock.Now(ctx).UTC())
   185  			Convey("Update again", func() {
   186  				testutil.InjectSelfConfigs(ctx, map[string]proto.Message{
   187  					common.ServiceRegistryFilePath: &cfgcommonpb.ServicesCfg{
   188  						Services: []*cfgcommonpb.Service{
   189  							{
   190  								Id:     serviceName,
   191  								Owners: []string{"owner@example.com"},
   192  							},
   193  						},
   194  					},
   195  				})
   196  				So(Update(ctx), ShouldBeNil)
   197  				service := &model.Service{
   198  					Name: serviceName,
   199  				}
   200  				So(datastore.Get(ctx, service), ShouldBeNil)
   201  				So(service.Info, ShouldResembleProto, &cfgcommonpb.Service{
   202  					Id:     serviceName,
   203  					Owners: []string{"owner@example.com"},
   204  				})
   205  			})
   206  		})
   207  
   208  		Convey("Update self", func() {
   209  			serviceInfo := &cfgcommonpb.Service{
   210  				Id:       testutil.AppID,
   211  				Hostname: ts.Host,
   212  				// Add the service endpoint to ensure LUCI Config update its own
   213  				// Service entity without making rpc call to itself.
   214  			}
   215  			testutil.InjectSelfConfigs(ctx, map[string]proto.Message{
   216  				common.ServiceRegistryFilePath: &cfgcommonpb.ServicesCfg{
   217  					Services: []*cfgcommonpb.Service{
   218  						serviceInfo,
   219  					},
   220  				},
   221  			})
   222  			validation.Rules.Add("exact:services/"+testutil.AppID, "exact:foo.cfg", func(ctx *validation.Context, configSet, path string, content []byte) error { return nil })
   223  			service := &model.Service{
   224  				Name: testutil.AppID,
   225  			}
   226  			So(Update(ctx), ShouldBeNil)
   227  			So(datastore.Get(ctx, service), ShouldBeNil)
   228  			So(service.Info, ShouldResembleProto, serviceInfo)
   229  			So(service.Metadata, ShouldResembleProto, &cfgcommonpb.ServiceMetadata{
   230  				ConfigPatterns: []*cfgcommonpb.ConfigPattern{
   231  					{
   232  						ConfigSet: "exact:services/" + testutil.AppID,
   233  						Path:      "exact:foo.cfg",
   234  					},
   235  				},
   236  			})
   237  			So(service.UpdateTime, ShouldEqual, clock.Now(ctx).UTC())
   238  		})
   239  
   240  		Convey("Delete Service entity for deleted service", func() {
   241  			So(Update(ctx), ShouldBeNil)
   242  			er, err := datastore.Exists(ctx, datastore.MakeKey(ctx, model.ServiceKind, serviceName))
   243  			So(err, ShouldBeNil)
   244  			So(er.All(), ShouldBeTrue)
   245  			testutil.InjectSelfConfigs(ctx, map[string]proto.Message{
   246  				common.ServiceRegistryFilePath: &cfgcommonpb.ServicesCfg{
   247  					Services: []*cfgcommonpb.Service{}, // delete the existing Service
   248  				},
   249  			})
   250  			So(Update(ctx), ShouldBeNil)
   251  			er, err = datastore.Exists(ctx, datastore.MakeKey(ctx, model.ServiceKind, serviceName))
   252  			So(err, ShouldBeNil)
   253  			So(er.Any(), ShouldBeFalse)
   254  		})
   255  
   256  		Convey("Legacy Metadata", func() {
   257  			legacyMetadata := &cfgcommonpb.ServiceDynamicMetadata{
   258  				Version: "1.0",
   259  				Validation: &cfgcommonpb.Validator{
   260  					Url: "https://example.com/validate",
   261  					Patterns: []*cfgcommonpb.ConfigPattern{
   262  						{ConfigSet: string(config.MustProjectSet("foo")), Path: "exact:bar.cfg"},
   263  					},
   264  				},
   265  				SupportsGzipCompression: true,
   266  			}
   267  			var legacySrvErrMsg string
   268  
   269  			legacyTestSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   270  				if legacySrvErrMsg != "" {
   271  					w.WriteHeader(http.StatusInternalServerError)
   272  					fmt.Fprint(w, legacySrvErrMsg)
   273  					return
   274  				}
   275  
   276  				switch bytes, err := protojson.Marshal(legacyMetadata); {
   277  				case err != nil:
   278  					w.WriteHeader(http.StatusInternalServerError)
   279  					fmt.Fprintf(w, "%s", err)
   280  				default:
   281  					w.Write(bytes)
   282  				}
   283  			}))
   284  			defer legacyTestSrv.Close()
   285  
   286  			serviceInfo := &cfgcommonpb.Service{
   287  				Id:          serviceName,
   288  				MetadataUrl: legacyTestSrv.URL,
   289  			}
   290  			testutil.InjectSelfConfigs(ctx, map[string]proto.Message{
   291  				common.ServiceRegistryFilePath: &cfgcommonpb.ServicesCfg{
   292  					Services: []*cfgcommonpb.Service{
   293  						serviceInfo,
   294  					},
   295  				},
   296  			})
   297  
   298  			Convey("First time update", func() {
   299  				service := &model.Service{
   300  					Name: serviceName,
   301  				}
   302  				So(datastore.Get(ctx, service), ShouldErrLike, datastore.ErrNoSuchEntity)
   303  				So(Update(ctx), ShouldBeNil)
   304  				So(datastore.Get(ctx, service), ShouldBeNil)
   305  				So(service.Info, ShouldResembleProto, serviceInfo)
   306  				So(service.LegacyMetadata, ShouldResembleProto, legacyMetadata)
   307  				So(service.UpdateTime, ShouldEqual, clock.Now(ctx).UTC())
   308  			})
   309  
   310  			Convey("Update existing", func() {
   311  				So(Update(ctx), ShouldBeNil)
   312  				Convey("Service info changed", func() {
   313  					updatedInfo := proto.Clone(serviceInfo).(*cfgcommonpb.Service)
   314  					updatedInfo.Owners = append(updatedInfo.Owners, "new-owner@example.com")
   315  					testutil.InjectSelfConfigs(ctx, map[string]proto.Message{
   316  						common.ServiceRegistryFilePath: &cfgcommonpb.ServicesCfg{
   317  							Services: []*cfgcommonpb.Service{
   318  								updatedInfo,
   319  							},
   320  						},
   321  					})
   322  					So(Update(ctx), ShouldBeNil)
   323  					service := &model.Service{
   324  						Name: serviceName,
   325  					}
   326  					So(datastore.Get(ctx, service), ShouldBeNil)
   327  					So(service.Info, ShouldResembleProto, updatedInfo)
   328  					So(service.LegacyMetadata, ShouldResembleProto, legacyMetadata)
   329  					So(service.UpdateTime, ShouldEqual, clock.Now(ctx).UTC())
   330  				})
   331  				Convey("Legacy metadata changed", func() {
   332  					legacyMetadata = proto.Clone(legacyMetadata).(*cfgcommonpb.ServiceDynamicMetadata)
   333  					legacyMetadata.SupportsGzipCompression = false
   334  					So(Update(ctx), ShouldBeNil)
   335  					service := &model.Service{
   336  						Name: serviceName,
   337  					}
   338  					So(datastore.Get(ctx, service), ShouldBeNil)
   339  					So(service.Info, ShouldResembleProto, serviceInfo)
   340  					So(service.LegacyMetadata, ShouldResembleProto, legacyMetadata)
   341  					So(service.UpdateTime, ShouldEqual, clock.Now(ctx).UTC())
   342  				})
   343  			})
   344  
   345  			Convey("Error for invalid legacy metadata", func() {
   346  				Convey("Invalid regex", func() {
   347  					legacyMetadata = proto.Clone(legacyMetadata).(*cfgcommonpb.ServiceDynamicMetadata)
   348  					legacyMetadata.Validation.Patterns = []*cfgcommonpb.ConfigPattern{
   349  						{ConfigSet: string(config.MustProjectSet("foo")), Path: "regex:["},
   350  					}
   351  				})
   352  				Convey("Empty url", func() {
   353  					legacyMetadata = proto.Clone(legacyMetadata).(*cfgcommonpb.ServiceDynamicMetadata)
   354  					legacyMetadata.Validation.Url = ""
   355  				})
   356  				Convey("Invalid url", func() {
   357  					legacyMetadata = proto.Clone(legacyMetadata).(*cfgcommonpb.ServiceDynamicMetadata)
   358  					legacyMetadata.Validation.Url = "http://example.com\\validate"
   359  				})
   360  				So(Update(ctx), ShouldErrLike, "invalid legacy metadata for service")
   361  			})
   362  
   363  			Convey("Upgrade from legacy to new", func() {
   364  				So(Update(ctx), ShouldBeNil)
   365  				service := &model.Service{
   366  					Name: serviceName,
   367  				}
   368  				So(datastore.Get(ctx, service), ShouldBeNil)
   369  				So(service.Info, ShouldResembleProto, serviceInfo)
   370  				So(service.Metadata, ShouldBeNil)
   371  				So(service.LegacyMetadata, ShouldNotBeNil)
   372  
   373  				newInfo := proto.Clone(serviceInfo).(*cfgcommonpb.Service)
   374  				newInfo.Hostname = ts.Host
   375  				newInfo.MetadataUrl = ""
   376  				testutil.InjectSelfConfigs(ctx, map[string]proto.Message{
   377  					common.ServiceRegistryFilePath: &cfgcommonpb.ServicesCfg{
   378  						Services: []*cfgcommonpb.Service{
   379  							newInfo,
   380  						},
   381  					},
   382  				})
   383  				So(Update(ctx), ShouldBeNil)
   384  				service = &model.Service{
   385  					Name: serviceName,
   386  				}
   387  				So(datastore.Get(ctx, service), ShouldBeNil)
   388  				So(service.Info, ShouldResembleProto, newInfo)
   389  				So(service.Metadata, ShouldNotBeNil)
   390  				So(service.LegacyMetadata, ShouldBeNil)
   391  			})
   392  		})
   393  	})
   394  }