Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let appservice registration files contain arbitrary keys #682

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 and sender_localpart are 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 @@ -183,6 +183,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 @@ -314,12 +314,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 @@ -395,7 +389,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 @@ -409,27 +402,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 @@ -124,26 +126,26 @@ func (d *Deployer) CreateDirtyDeployment() (*Deployment, error) {
}, nil
}

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 @@ -160,12 +162,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 @@ -285,7 +294,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 @@ -374,8 +383,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 @@ -422,12 +435,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),
Network: networkName,
}
Expand Down
6 changes: 3 additions & 3 deletions internal/docker/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ 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 ..."} }
DeviceIDs map[string]string // e.g { "@alice:hs1": "myDeviceID" }
ApplicationServices map[string]map[string]interface{} // e.g { "my-as-id": { "id": "foo", "as_token": "bar", ... } }
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
CSAPIClients []*client.CSAPI
Expand Down Expand Up @@ -189,7 +189,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 @@ -110,7 +111,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 All @@ -135,7 +136,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
Loading