diff --git a/b/blueprints.go b/b/blueprints.go index 9b2ce150..62ba59da 100644 --- a/b/blueprints.go +++ b/b/blueprints.go @@ -52,8 +52,12 @@ type Homeserver struct { Users []User // The list of rooms to create on this homeserver Rooms []Room - // The list of application services to create on the homeserver - ApplicationServices []ApplicationService + // The list of application services to create on the homeserver. + // The maps are the registration file YAML contents. This does not have + // its own struct so we can support MSC extensions which have unknown fields + // e.g MSC2409 has push_ephemeral: true in the registration. The fields + // hs_token and as_token will be automatically generated. + ApplicationServices []map[string]interface{} // Optionally override the baseImageURI for blueprint creation BaseImageURI *string } @@ -83,15 +87,6 @@ type Room struct { Events []Event } -type ApplicationService struct { - ID string - HSToken string - ASToken string - URL string - SenderLocalpart string - RateLimited bool -} - type Event struct { Type string `json:"type"` Sender string `json:"sender,omitempty"` @@ -130,10 +125,22 @@ func Validate(bp Blueprint) (Blueprint, error) { } } for i, as := range hs.ApplicationServices { - hs.ApplicationServices[i], err = normalizeApplicationService(as) + hs.ApplicationServices[i], err = generateAppServiceTokens(as) if err != nil { return bp, err } + // ID is required + requiredStringFields := []string{"id", "sender_localpart"} + for _, required := range requiredStringFields { + val, ok := as[required] + if !ok { + return bp, fmt.Errorf("ApplicationService[%d] missing required field '%s'", i, required) + } + _, ok = val.(string) + if !ok { + return bp, fmt.Errorf("ApplicationService[%d] required field '%s' must be a string but it isn't", i, required) + } + } } } @@ -181,7 +188,7 @@ func normaliseUser(u string, hsName string) (string, error) { return u, nil } -func normalizeApplicationService(as ApplicationService) (ApplicationService, error) { +func generateAppServiceTokens(as map[string]interface{}) (map[string]interface{}, error) { hsToken := make([]byte, 32) _, err := rand.Read(hsToken) if err != nil { @@ -194,8 +201,8 @@ func normalizeApplicationService(as ApplicationService) (ApplicationService, err return as, err } - as.HSToken = hex.EncodeToString(hsToken) - as.ASToken = hex.EncodeToString(asToken) + as["hs_token"] = hex.EncodeToString(hsToken) + as["as_token"] = hex.EncodeToString(asToken) return as, err } diff --git a/b/hs_with_application_service.go b/b/hs_with_application_service.go index 2bb7b154..54e3bf21 100644 --- a/b/hs_with_application_service.go +++ b/b/hs_with_application_service.go @@ -16,12 +16,12 @@ var BlueprintHSWithApplicationService = MustValidate(Blueprint{ DisplayName: "Bob", }, }, - ApplicationServices: []ApplicationService{ + ApplicationServices: []map[string]interface{}{ { - ID: "my_as_id", - URL: "http://localhost:9000", - SenderLocalpart: "the-bridge-user", - RateLimited: false, + "id": "my_as_id", + "url": "http://localhost:9000", + "sender_localpart": "the-bridge-user", + "rate_limited": false, }, }, }, diff --git a/cmd/homerunner/setup.go b/cmd/homerunner/setup.go index 2daf28b8..42da6de4 100644 --- a/cmd/homerunner/setup.go +++ b/cmd/homerunner/setup.go @@ -48,7 +48,7 @@ func (r *Runtime) CreateDeployment(imageURI string, blueprint *b.Blueprint) (*do if err != nil { return nil, expires, fmt.Errorf("CreateDeployment: NewDeployer returned error %s", err) } - dep, err := d.Deploy(context.Background(), blueprint.Name) + dep, err := d.Deploy(context.Background(), *blueprint) if err != nil { return nil, expires, fmt.Errorf("CreateDeployment: Deploy returned error %s", err) } diff --git a/cmd/perftest/test.go b/cmd/perftest/test.go index ca1f0ac5..03987f13 100644 --- a/cmd/perftest/test.go +++ b/cmd/perftest/test.go @@ -30,7 +30,7 @@ func runTest(testName string, builder *docker.Builder, deployer *docker.Deployer if err := builder.ConstructBlueprintIfNotExist(b.BlueprintCleanHS); err != nil { return nil, err } - deployment, err := deployer.Deploy(context.Background(), b.BlueprintCleanHS.Name) + deployment, err := deployer.Deploy(context.Background(), b.BlueprintCleanHS) if err != nil { return nil, err } diff --git a/go.mod b/go.mod index 288fe9e6..be97653c 100644 --- a/go.mod +++ b/go.mod @@ -42,5 +42,6 @@ require ( golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.0.3 // indirect ) diff --git a/go.sum b/go.sum index c885e3f3..c2a4285d 100644 --- a/go.sum +++ b/go.sum @@ -176,6 +176,7 @@ gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= diff --git a/internal/docker/builder.go b/internal/docker/builder.go index 9a2f86b3..0c063cc0 100644 --- a/internal/docker/builder.go +++ b/internal/docker/builder.go @@ -313,12 +313,6 @@ func (d *Builder) construct(bprint b.Blueprint) (errs []error) { labels["device_id"+userID] = deviceID } - // Combine the labels for tokens and application services - asLabels := labelsForApplicationServices(res.homeserver) - for k, v := range asLabels { - labels[k] = v - } - // Stop the container before we commit it. // This gives it chance to shut down gracefully. // If we don't do this, then e.g. Postgres databases can become corrupt, which @@ -392,7 +386,6 @@ func (d *Builder) constructHomeserver(blueprintName string, runner *instruction. // deployBaseImage runs the base image and returns the baseURL, containerID or an error. func (d *Builder) deployBaseImage(blueprintName string, hs b.Homeserver, contextStr, networkName string) (*HomeserverDeployment, error) { - asIDToRegistrationMap := asIDToRegistrationFromLabels(labelsForApplicationServices(hs)) var baseImageURI string if hs.BaseImageURI == nil { baseImageURI = d.Config.BaseImageURI @@ -406,27 +399,11 @@ func (d *Builder) deployBaseImage(blueprintName string, hs b.Homeserver, context return deployImage( d.Docker, baseImageURI, fmt.Sprintf("complement_%s", contextStr), - d.Config.PackageNamespace, blueprintName, hs.Name, asIDToRegistrationMap, contextStr, + d.Config.PackageNamespace, blueprintName, hs.Name, hs.ApplicationServices, contextStr, networkName, d.Config, ) } -// Multilines label using Dockerfile syntax is unsupported, let's inline \n instead -func generateASRegistrationYaml(as b.ApplicationService) string { - return fmt.Sprintf("id: %s\\n", as.ID) + - fmt.Sprintf("hs_token: %s\\n", as.HSToken) + - fmt.Sprintf("as_token: %s\\n", as.ASToken) + - fmt.Sprintf("url: '%s'\\n", as.URL) + - fmt.Sprintf("sender_localpart: %s\\n", as.SenderLocalpart) + - fmt.Sprintf("rate_limited: %v\\n", as.RateLimited) + - "namespaces:\\n" + - " users:\\n" + - " - exclusive: false\\n" + - " regex: .*\\n" + - " rooms: []\\n" + - " aliases: []\\n" -} - // createNetworkIfNotExists creates a docker network and returns its name. // Name is guaranteed not to be empty when err == nil func createNetworkIfNotExists(docker *client.Client, pkgNamespace, blueprintName string) (networkName string, err error) { diff --git a/internal/docker/deployer.go b/internal/docker/deployer.go index 6af22d74..583444b9 100644 --- a/internal/docker/deployer.go +++ b/internal/docker/deployer.go @@ -32,7 +32,9 @@ import ( "github.com/docker/docker/client" "github.com/docker/go-connections/nat" + "github.com/matrix-org/complement/b" complementRuntime "github.com/matrix-org/complement/runtime" + "gopkg.in/yaml.v3" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -76,26 +78,26 @@ func (d *Deployer) log(str string, args ...interface{}) { log.Printf(str, args...) } -func (d *Deployer) Deploy(ctx context.Context, blueprintName string) (*Deployment, error) { +func (d *Deployer) Deploy(ctx context.Context, blueprint b.Blueprint) (*Deployment, error) { dep := &Deployment{ Deployer: d, - BlueprintName: blueprintName, + BlueprintName: blueprint.Name, HS: make(map[string]*HomeserverDeployment), Config: d.config, } images, err := d.Docker.ImageList(ctx, types.ImageListOptions{ Filters: label( "complement_pkg="+d.config.PackageNamespace, - "complement_blueprint="+blueprintName, + "complement_blueprint="+blueprint.Name, ), }) if err != nil { return nil, fmt.Errorf("Deploy: failed to ImageList: %w", err) } if len(images) == 0 { - return nil, fmt.Errorf("Deploy: No images have been built for blueprint %s", blueprintName) + return nil, fmt.Errorf("Deploy: No images have been built for blueprint %s", blueprint.Name) } - networkName, err := createNetworkIfNotExists(d.Docker, d.config.PackageNamespace, blueprintName) + networkName, err := createNetworkIfNotExists(d.Docker, d.config.PackageNamespace, blueprint.Name) if err != nil { return nil, fmt.Errorf("Deploy: %w", err) } @@ -112,12 +114,19 @@ func (d *Deployer) Deploy(ctx context.Context, blueprintName string) (*Deploymen mu.Unlock() contextStr := img.Labels["complement_context"] hsName := img.Labels["complement_hs_name"] - asIDToRegistrationMap := asIDToRegistrationFromLabels(img.Labels) + // find appservices + var appServices []map[string]interface{} + for _, hs := range blueprint.Homeservers { + if hs.Name == hsName { + appServices = hs.ApplicationServices + break + } + } // TODO: Make CSAPI port configurable deployment, err := deployImage( d.Docker, img.ID, fmt.Sprintf("complement_%s_%s_%s_%d", d.config.PackageNamespace, d.DeployNamespace, contextStr, counter), - d.config.PackageNamespace, blueprintName, hsName, asIDToRegistrationMap, contextStr, networkName, d.config, + d.config.PackageNamespace, blueprint.Name, hsName, appServices, contextStr, networkName, d.config, ) if err != nil { if deployment != nil && deployment.ContainerID != "" { @@ -232,7 +241,7 @@ func (d *Deployer) Restart(hsDep *HomeserverDeployment, cfg *config.Complement) // nolint func deployImage( docker *client.Client, imageID string, containerName, pkgNamespace, blueprintName, hsName string, - asIDToRegistrationMap map[string]string, contextStr, networkName string, cfg *config.Complement, + appServices []map[string]interface{}, contextStr, networkName string, cfg *config.Complement, ) (*HomeserverDeployment, error) { ctx := context.Background() var extraHosts []string @@ -321,8 +330,12 @@ func deployImage( } // Create the application service files - for asID, registration := range asIDToRegistrationMap { - err = copyToContainer(docker, containerID, fmt.Sprintf("%s%s.yaml", MountAppServicePath, url.PathEscape(asID)), []byte(registration)) + for _, appService := range appServices { + contents, err := yaml.Marshal(appService) + if err != nil { + return stubDeployment, err + } + err = copyToContainer(docker, containerID, fmt.Sprintf("%s%s.yaml", MountAppServicePath, appService["id"]), contents) if err != nil { return stubDeployment, err } @@ -369,12 +382,17 @@ func deployImage( ) } + appServicesMap := make(map[string]map[string]interface{}) + for i, as := range appServices { + appServicesMap[as["id"].(string)] = appServices[i] + } + d := &HomeserverDeployment{ BaseURL: baseURL, FedBaseURL: fedBaseURL, ContainerID: containerID, AccessTokens: tokensFromLabels(inspect.Config.Labels), - ApplicationServices: asIDToRegistrationFromLabels(inspect.Config.Labels), + ApplicationServices: appServicesMap, DeviceIDs: deviceIDsFromLabels(inspect.Config.Labels), } diff --git a/internal/docker/deployment.go b/internal/docker/deployment.go index 3847d61f..9c538943 100644 --- a/internal/docker/deployment.go +++ b/internal/docker/deployment.go @@ -36,7 +36,7 @@ type HomeserverDeployment struct { ContainerID string // e.g 10de45efba AccessTokens map[string]string // e.g { "@alice:hs1": "myAcc3ssT0ken" } accessTokensMutex sync.RWMutex - ApplicationServices map[string]string // e.g { "my-as-id": "id: xxx\nas_token: xxx ..."} } + ApplicationServices map[string]map[string]interface{} DeviceIDs map[string]string // e.g { "@alice:hs1": "myDeviceID" } // track all clients so if Restart() is called we can repoint to the new high-numbered port @@ -178,7 +178,7 @@ func (d *Deployment) UnauthenticatedClient(t *testing.T, hsName string) *client. } // AppServiceUser returns a client for the given app service user ID. The HS in question must have an appservice -// hooked up to it already. TODO: REMOVE +// hooked up to it already. func (d *Deployment) AppServiceUser(t *testing.T, hsName, appServiceUserID string) *client.CSAPI { t.Helper() dep, ok := d.HS[hsName] diff --git a/internal/docker/labels.go b/internal/docker/labels.go index 98f5caf9..0963a5ba 100644 --- a/internal/docker/labels.go +++ b/internal/docker/labels.go @@ -4,8 +4,6 @@ import ( "strings" "github.com/docker/docker/api/types/filters" - - "github.com/matrix-org/complement/b" ) // label returns a filter for the presence of certain labels ("complement_context") or a match of @@ -29,29 +27,6 @@ func tokensFromLabels(labels map[string]string) map[string]string { return userIDToToken } -func asIDToRegistrationFromLabels(labels map[string]string) map[string]string { - asMap := make(map[string]string) - for k, v := range labels { - if strings.HasPrefix(k, "application_service_") { - // cf comment of generateASRegistrationYaml for ReplaceAll explanation - asMap[strings.TrimPrefix(k, "application_service_")] = strings.ReplaceAll(v, "\\n", "\n") - } - } - return asMap -} - -func labelsForApplicationServices(hs b.Homeserver) map[string]string { - labels := make(map[string]string) - // collect and store app service registrations as labels 'application_service_$as_id: $registration' - // collect and store app service access tokens as labels 'access_token_$sender_localpart: $as_token' - for _, as := range hs.ApplicationServices { - labels["application_service_"+as.ID] = generateASRegistrationYaml(as) - - labels["access_token_@"+as.SenderLocalpart+":"+hs.Name] = as.ASToken - } - return labels -} - func deviceIDsFromLabels(labels map[string]string) map[string]string { userIDToToken := make(map[string]string) for k, v := range labels { diff --git a/test_package.go b/test_package.go index 0003120c..f7ca5df4 100644 --- a/test_package.go +++ b/test_package.go @@ -28,7 +28,8 @@ type Deployment interface { // an existing logged in client must be supplied. Login(t *testing.T, hsName string, existing *client.CSAPI, opts helpers.LoginOpts) *client.CSAPI // AppServiceUser returns a client for the given app service user ID. The HS in question must have an appservice - // hooked up to it already. TODO: REMOVE + // hooked up to it already. The user ID can be the application service itself, or a controlled user in the application + // service's namespace. AppServiceUser(t *testing.T, hsName, appServiceUserID string) *client.CSAPI // Restart a deployment. Restart(t *testing.T) error @@ -109,7 +110,7 @@ func (tp *TestPackage) OldDeploy(t *testing.T, blueprint b.Blueprint) Deployment t.Fatalf("OldDeploy: NewDeployer returned error %s", err) } timeStartDeploy := time.Now() - dep, err := d.Deploy(context.Background(), blueprint.Name) + dep, err := d.Deploy(context.Background(), blueprint) if err != nil { t.Fatalf("OldDeploy: Deploy returned error %s", err) } @@ -138,7 +139,7 @@ func (tp *TestPackage) Deploy(t *testing.T, numServers int) Deployment { t.Fatalf("Deploy: NewDeployer returned error %s", err) } timeStartDeploy := time.Now() - dep, err := d.Deploy(context.Background(), blueprint.Name) + dep, err := d.Deploy(context.Background(), blueprint) if err != nil { t.Fatalf("Deploy: Deploy returned error %s", err) }