Skip to content

Commit

Permalink
Let appservice registration files contain arbitrary keys
Browse files Browse the repository at this point in the history
- Remove storing/retrieving AS registrations from docker labels, there's no need for this.
- Remove `b.ApplicationService` so arbitrary keys can be set on the registration file.
  • Loading branch information
kegsay committed Oct 18, 2023
1 parent 39b3b9d commit a62dc11
Show file tree
Hide file tree
Showing 11 changed files with 67 additions and 87 deletions.
37 changes: 22 additions & 15 deletions b/blueprints.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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)
}
}
}
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
10 changes: 5 additions & 5 deletions b/hs_with_application_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion cmd/homerunner/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/perftest/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
25 changes: 1 addition & 24 deletions internal/docker/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down
40 changes: 29 additions & 11 deletions internal/docker/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand All @@ -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 != "" {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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),
}

Expand Down
4 changes: 2 additions & 2 deletions internal/docker/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
25 changes: 0 additions & 25 deletions internal/docker/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
7 changes: 4 additions & 3 deletions test_package.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down

0 comments on commit a62dc11

Please sign in to comment.