Skip to content

Support custom properties on teams#609

Merged
andrewstillv15 merged 12 commits intomainfrom
support-custom-properties-on-teams
Mar 6, 2026
Merged

Support custom properties on teams#609
andrewstillv15 merged 12 commits intomainfrom
support-custom-properties-on-teams

Conversation

@andrewstillv15
Copy link
Contributor

@andrewstillv15 andrewstillv15 commented Mar 5, 2026

Resolves #12820

Problem

We need to support custom properties for teams

Solution

Add custom property CRU operations for teams. Added a new service.GetProperty as the original GetProperty while following the Opslevel schema was misleading as it appeared to support teams.

Test code for your enjoyment:

func TeamPropertyTest(client *ol.Client) {
	fmt.Println("=== Testing custom properties on teams ===")

	// 1. Find a team to test with
	teams, err := client.ListTeams(nil)
	if err != nil {
		fmt.Printf("❌ Failed to list teams: %v\n", err)
		return
	}
	if len(teams.Nodes) == 0 {
		fmt.Println("❌ No teams found")
		return
	}
	team := teams.Nodes[0]
	fmt.Printf("Using team: %s (ID: %s)\n", team.Alias, team.Id)

	// 2. Create a property definition
	schema, err := ol.NewJSONSchema(`{"type":"string"}`)
	if err != nil {
		fmt.Printf("❌ Failed to create JSON schema: %v\n", err)
		return
	}
	def, err := client.CreateTeamPropertyDefinition(ol.TeamPropertyDefinitionInput{
		Alias:  "opslevel-go-team-test",
		Name:   "opslevel-go-team-test",
		Schema: *schema,
	})
	if err != nil {
		fmt.Printf("❌ Failed to create property definition: %v\n", err)
		return
	}
	fmt.Printf("✅ Created property definition: %s (ID: %s)\n", def.Name, def.Id)

	// 3. Assign the property to the team
	property, err := client.PropertyAssign(ol.PropertyInput{
		Owner:      ol.IdentifierInput{Id: &team.Id},
		Definition: ol.IdentifierInput{Id: &def.Id},
		OwnerType:  &ol.PropertyOwnerTypeEnumTeam,
		Value:      ol.JsonString(`"hello from opslevel-go"`),
	})
	if err != nil {
		fmt.Printf("❌ Failed to assign property to team: %v\n", err)
		if _, err := client.AssignTeamPropertyDefinitions(ol.TeamPropertyDefinitionsAssignInput{
			Properties: []ol.TeamPropertyDefinitionInput{},
		}); err != nil {
			fmt.Printf("⚠️  Failed to clear team property definitions: %v\n", err)
		} else {
			fmt.Println("✅ Cleared team property definitions")
		}
		return
	}
	fmt.Printf("✅ Assigned property to team, value: %s\n", *property.Value)

	// 4. Retrieve the property back
	retrieved, err := team.GetProperty(client, string(def.Id))
	if err != nil {
		fmt.Printf("❌ Failed to get property: %v\n", err)
		if _, err := client.AssignTeamPropertyDefinitions(ol.TeamPropertyDefinitionsAssignInput{
			Properties: []ol.TeamPropertyDefinitionInput{},
		}); err != nil {
			fmt.Printf("⚠️  Failed to clear team property definitions: %v\n", err)
		} else {
			fmt.Println("✅ Cleared team property definitions")
		}
		return
	}
	fmt.Printf("✅ Retrieved property, value: %s\n", *retrieved.Value)

	// 5. List properties on the team
	properties, err := team.GetProperties(client, nil)
	if err != nil {
		fmt.Printf("❌ Failed to list team properties: %v\n", err)
		if _, err := client.AssignTeamPropertyDefinitions(ol.TeamPropertyDefinitionsAssignInput{
			Properties: []ol.TeamPropertyDefinitionInput{},
		}); err != nil {
			fmt.Printf("⚠️  Failed to clear team property definitions: %v\n", err)
		} else {
			fmt.Println("✅ Cleared team property definitions")
		}
		return
	}
	found := false
	for _, p := range properties.Nodes {
		if p.Definition.Id == def.Id {
			found = true
			break
		}
	}
	if !found {
		fmt.Printf("❌ Property not found in team.GetProperties() results\n")
	} else {
		fmt.Printf("✅ Property found in team.GetProperties() (%d total properties on team)\n", len(properties.Nodes))
	}

	// 6. Unassign the property
	err = client.PropertyUnassign(string(team.Id), string(def.Id))
	if err != nil {
		fmt.Printf("❌ Failed to unassign property: %v\n", err)
	} else {
		fmt.Println("✅ Property unassigned from team")
	}

	// 7. Cleanup - clear all team property definitions
	if _, err := client.AssignTeamPropertyDefinitions(ol.TeamPropertyDefinitionsAssignInput{
		Properties: []ol.TeamPropertyDefinitionInput{},
	}); err != nil {
		fmt.Printf("⚠️  Failed to clear team property definitions: %v\n", err)
	} else {
		fmt.Println("✅ Cleared team property definitions")
	}

	fmt.Println("\n✅ Team property test complete")
}

Checklist

  • I have run this code, and it appears to resolve the stated issue.
  • This PR does not reduce total test coverage
  • This PR has no user interface changes or has already received approval from product management to change the interface.
  • Does this change require a Terraform schema change?
    • If so what is the ticket or PR #
  • Make a changie entry that explains the customer facing outcome of this change

Copy link
Contributor

@derek-etherton-opslevel derek-etherton-opslevel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just a bug with team.GetProperties TotalCount always returning 0 - it's also bugged on service.GetProperties if we could fix both

Definition PropertyDefinitionId `graphql:"definition"`
Locked bool `graphql:"locked"`
Owner EntityOwnerService `graphql:"owner"`
Owner PropertyOwner `graphql:"owner"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a small breaking change where like property.Owner.Aliases would work before, but now you'd have to do property.Owner.ServiceId.Aliases. I think it makes sense though, maybe just ensure we record it as a breaking change in changelog? I'm not 100% sure how we handle these historically.

team.go Outdated
if err != nil {
return nil, err
}
team.Properties.TotalCount += resp.TotalCount
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like the way we're loading properties totalCount on services is wrong, and we cargo culted it here. This is just a recursive loop so totalCount is always 0 - I think we yoinked a ListX pattern by accident, which works a bit differently.

I think we can just set totalCount at the very end, like: service.Properties.TotalCount = len(service.Properties.Nodes)

andrewstillv15 and others added 2 commits March 5, 2026 22:17
TotalCount is tagged graphql:"-" so it's never populated from the API.
The recursive accumulation pattern (TotalCount += resp.TotalCount) always
resulted in 0. Fix by setting TotalCount = len(Nodes) after all pages
have been accumulated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@andrewstillv15 andrewstillv15 merged commit 0456aab into main Mar 6, 2026
4 checks passed
@andrewstillv15 andrewstillv15 deleted the support-custom-properties-on-teams branch March 6, 2026 03:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants