Skip to content

Add OS Package Repo Tool 2 (OPRT2) - #400

Closed
fheinecke wants to merge 5 commits into
mainfrom
fred/oprt2-1
Closed

Add OS Package Repo Tool 2 (OPRT2)#400
fheinecke wants to merge 5 commits into
mainfrom
fred/oprt2-1

Conversation

@fheinecke

Copy link
Copy Markdown
Contributor

Summary

This is a first pass at a new revision of the OS Package Repo Tool (OPRT). The primary purpose of this is to public OS packages via Attune, and is intended to replace practically all of our current tooling around OS package publishing.

Goals

As discussed internally, this tool will operate under the following assumptions:

  • AWS auth is handled outside of program (this is environment specific)
  • Teleport auth is handled outside of program (this is environment specific)
  • Configuration is provided outside of program (this is environment specific)
  • Required binaries (attune CLI, Teleport CLI, others required in the future) will be present in the execution environment

Also discussed internally, this tool is designed to meet the following criteria:

  • It must be able to run both locally and in CI/CD pipelines
    • This affects where packages will be pulled from as well as how Attune authentication is handled
  • It must run significantly faster than our current process
    • This affects what code needs to run concurrently
  • It must support our disaster recovery (rebuilding the entire prod repo from scratch)
    • This affects everything related to supporting a longer runtime, like credential renewal
  • It must not require running through a Teleport-backed proxy (e.g. tsh proxy app attune)
  • It must "nicely" support both being ran as a CLI tool with our current CI/CD pipelines, and should support being used as a library for our replacement of GHA (whatever this ends up looking like)

The only component in this PR that is not built to meet these requirements is config file validation (see schema dir). However I believe that this is important to make it easier to detect problems with changes to our CI/CD pipelines. I also believe that this is less complex and easier to read than re-implementing full config validation and defaulting outside of the jsonschema library.

Remaining items

This is missing the following pieces that I will add in follow up PRs after this gets an initial review:

  • Tests
  • A README.md (I will try to add answers to functional questions asked by reviewers to this)
  • A GitHub Action and/or reusable workflow to run this in our current pipelines

Usage

The tool is intended to be used as follows (GHA wrapper to come):

Config file:

---
# yaml-language-server: $schema=./config.json
logger:
  level: debug
attune:
  authentication:
    mTLS:
      endpoint: https://attune.ci-cd-cluster.tld
      certificateSource:
        teleport:
          workloadIdentity:
            name: workload-id
packageManagers:
  - apt:
      fileSource:
        s3:
          bucket: some-bucket-name
          path: teleport/tags/
      components:
        # NOTE: this will require updating whenever a new major branch is cut.
        # I'm open to suggestions on making this easier... maybe templating of some kind?
        stable/v18: &major_version_files
          - teleport-(amd|arm)64\.deb
          - teleport-updater\.deb
          - other-packages\.deb
        stable/cloud:
          - teleport-amd64\.deb
        stable/rolling: *major_version_files
      # When adding a new supported OS, update the github.com/gravitational/teleport/blob/master/lib/web/scripts/node-join/install.sh script
      # Otherwise, it will keep using the binary installation instead of the deb repo.
      distros:
        # https://wiki.ubuntu.com/Releases for details
        ubuntu:
          - xenial # 16.04 LTS
          - bionic # 18.04 LTS
          - focal # 20.04 LTS
          - jammy # 22.04 LTS
          - noble # 24.04 LTS
          - plucky # 25.04
        # See https://wiki.debian.org/DebianReleases#Production_Releases for details
        debian:
          - bullseye # 11
          - bookworm # 12
          - trixie # 13
          - forky # 14

Run it:

# Install tools
apt install aws-cli teleport attune

# Platform-specific auth
aws sso login
tsh login

# Run the tool
oprt2 -c /path/to/config.yaml

@fheinecke
fheinecke requested a review from a team as a code owner September 25, 2025 20:20
@socket-security

socket-security Bot commented Sep 25, 2025

Copy link
Copy Markdown

Comment thread tools/oprt2/pkg/attunehooks/gpg/archive/archiveprovider.go Dismissed
} else {
// Reset the length to 0, but don't release the allocated space
soh.buffer = soh.buffer[:0]
cmd.Stdout = bytes.NewBuffer(soh.buffer)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we just make soh.buffer a bytes.Buffer itself? This form of NewBuffer is meant for when you need a buffer with some initial contents (which we don't have since we just cleared out any existing content).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this meant to be committed?

@@ -0,0 +1,13 @@
FROM mcr.microsoft.com/vscode/devcontainers/go:1.25

RUN go install gotest.tools/gotestsum@latest && \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need to install @latest here? Looks like we pin the version of gotestsum elsewhere in this PR.


logger, err := config.GetLogger(c.Logger)
if err != nil {
return fmt.Errorf("failed to create logger")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
return fmt.Errorf("failed to create logger")
return errors.New("failed to create logger")

For a static error message prefer errors.New over Errorf.

// Ensure that the cleanup hook is always run. This is important to avoid leaking Attune credentials.
if closer, ok := authenticator.(commandrunner.CleanupHook); ok {
defer func() {
cleanupCtx, cancel := context.WithTimeout(context.Background(), time.Minute)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are you intentionally wrapping context.Background() instead of using ctx here?

If so, context.WithoutCancel(ctx) is another option worth considering. It's a bit more explicit.

)

// EnvVarHook provides an easy way to set an environment variable on every
// command invokation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// command invokation.
// command invocation.

// isOPRT2ConfigValid validates that the provided config file contents match the JSON schema
// for [OPRT2] config. Returns true if the config is valid, false otherwise. Records error
// information to stderr.
func isOPRT2ConfigValid(configFileAsJSON []byte) bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Generally library code should return some error type (with as much detail as you'd like) and leave it to the caller to decide whether to write the detail to stderr or do something else with it.

// read and write functions will error, stopping the copy.
// This retains all properties of [io.Copy], including support for [io.WriterTo] and
// [io.ReaderFrom].
func Copy(ctx context.Context, dst io.Writer, src io.Reader) (int64, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This feels like an antipattern to me.

// If the context is cancelled, calls to
// read and write functions will error, stopping the copy.

How are you satisfying this? It looks like me like the context is only checked before the read or write operation begins, but as soon as it starts we stop respecting context cancelation and enter a blocking call.

return errors.Join(cleanupErrs...)
}

// ListItemsWithPrefix returns a list of items in the storage backend that match the given prefix.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
// ListItemsWithPrefix returns a list of items in the storage backend that match the given prefix.
// ListItems returns a list of items in the storage backend.

Comment on lines +81 to +82
keys := slices.Collect(maps.Keys(uniqueMatchers))
slices.Sort(keys)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
keys := slices.Collect(maps.Keys(uniqueMatchers))
slices.Sort(keys)
keys := slices.Sorted(maps.Keys(uniqueMatchers))

@r0mant r0mant left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@fheinecke Two requests:

  • +4K LoC is a lot to review. Please split this PR into several logical parts that are more manageable. Workflows, Makefiles, configs, tool itself can likely be split as well into several logical parts.
  • I only glanced through the changeset so far but it all seems very complex: contextualcopy.io, io.go, contextlock.go, mutexmap.go, providers, some schema generator? Why is all of this needed, can the implementation be simplified before we spend time on the review? I'm pretty sure even Teleport codebase doesn't have this many wrappers over standard library functions and primitives, I'm certain we can do just fine without any of those for the tool that's just supposed to auth with Teleport and call an API to push packages.

I encourage you to start very simple and massively simplify the implementation, ensure the tool has bare minimum and essential stuff (Start simple and iterate). We can always add complexity when needed. Thanks.

@fheinecke

Copy link
Copy Markdown
Contributor Author

Please split this PR into several logical parts that are more manageable. Workflows, Makefiles, configs, tool itself can likely be split as well into several logical parts.

Sure, I can do this.

Why is all of this needed, can the implementation be simplified before we spend time on the review?
We can always add complexity when needed.

As mentioned in the PR body the complexity of this is stemming from the following business requirements set prior to writing the first line of code for this project:

  • It must be able to run both locally and in CI/CD pipelines
    • This affects where packages will be pulled from as well as how Attune authentication is handled
  • It must run significantly faster than our current process
    • This affects what code needs to run concurrently
  • It must support our disaster recovery (rebuilding the entire prod repo from scratch)
    • This affects everything related to supporting a longer runtime, like credential renewal
  • It must not require running through a Teleport-backed proxy (e.g. tsh proxy app attune)
    • This necessitates basically building the TLS proxy implementation
  • It must "nicely" support both being ran as a CLI tool with our current CI/CD pipelines, and should support being used as a library for our replacement of GHA (whatever this ends up looking like)
    • This necessitates writing modular, reusable code

Yes, all of this could be simplified, but only if we either:

  • Cut back on the business asks, or
  • Shifted the complexity elsewhere (like fixing the Teleport product issues with uploading lots of large files quickly)

Is there something specific that you'd like me to change here?

I'm pretty sure even Teleport codebase doesn't have this many wrappers over standard library functions and primitives

Most if not all the contents of these files are here because:

  • There are either long-standing open requests for these against the Go stdlib
  • There were previously open requests for these against the Go stdlib and they were closed with the recommendation that they be implemented by users outside of the stdlib

I can link to these if you like. Basically all of them are here because the Go encourages/requires the use of a context.Context to cancel blocking requests, but the Go standard library is missing support for this in several critical places. Without this:

  • Secrets will be leaked in several cases, such as SIGINT/SIGKILL being sent to the program from the environment (CI or local user).
  • The program will not exit gracefully, and will hang in most error cases

@fheinecke fheinecke closed this Sep 26, 2025
@fheinecke

Copy link
Copy Markdown
Contributor Author

@zmb3 I'll copy over and address your review comments on the split/follow up PRs

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.

4 participants