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

Confidential computing attestation container #8428

Merged

Conversation

mackdk
Copy link
Contributor

@mackdk mackdk commented Mar 8, 2024

What does this PR change?

This PR defines a new microservice in java to handle attestation results and process them. It runs in a new separate container which is as well defined in this PR. The design is modular and the service can handle multiple attestation modules dynamically loaded at runtime. Each module is able to handle a specific attestation result type.

The java project is defined in Maven. I created a parent pom uyuni-java-parent where all the dependency versions are centralized. Then there are the following modules:

  • uyuni-java-common, which contains some basic functionality meant to be reusable across multiple java projects
  • coco-attestation, root project for the real attestation service divided into:
    • attestation-core, the main component handling the queue processor logic, module loading and process result handling.
    • attestation-module, contains the classes and the interfaces needed to implement an attestation module.
    • attestation-module-snpguest, the attestation module for snpguest (real implementation is still WIP).

Additional attestation modules will be implemented. The current implementation uses Postgresql notification to avoid polling the database table for data.

Open discussion points

  • Naming and modularization: The modularization was created in this way because this was meant to be a base that can be reused to write new services in java when they do not require the full uyuni codebase, but it's not currently strictly needed (a part from the separation attestation core/modules). The whole code is now contained in a microservices subfolder, which is not ideal. The right place should be of course java, but to my understanding it's not possible without moving the existing spacewalk-java package to a subfolder (which I guess we strongly don't want to do 🙂). Any suggestion regarding better naming and modularization is welcome.
  • Container: I tried to follow the structure and naming consistent with what we have in the containers folder. Please double check it.
  • Packaging: this is currently split into 3 OBS projects. All of them build with maven with a minor tweaking on the pom configuration.
    • uyuni-java-parent: which only contains the parent pom for building
    • uyuni-java-common: which packages the corresponding maven project
    • uyuni-coco-attestation: packages the actual service. The main package contains attestation-core and attestation-module, all attestation module will be separated into sub-packages. Currently only uyuni-coco-attestation-module-snpguest exists.
  • Versioning: for now I kept the default 1.0.0-SNAPSHOT. We need to figure out a versioning scheme and how to handle versioning when tagging.
  • Java dependencies: the code only uses libraries that were already part of uyuni. Currently the project depends on log4j 2, c3p0 for connection pooling, mybatis for database access and postgresql-jdbc for the database driver. Currently, uyuni relies on pgjdbc-ng for receiving notifications from the database, but the current driver offers a similar functionality and the former is stale and has been basically discontinued since three years ago since the authors are no longer using jdbc so they have no motivation to maintain it (What's the status and direction of the project? impossibl/pgjdbc-ng#579). Tests are using junit 5 and mockito (for which I'm using version 5.10.0 that is not provided as a package in OBS but it's not needed by the package build process nor of course at runtime). Additional dependencies might be considered to simplify further developments. In particular a dependency injection mechanism could greatly improve the code. Currently, class dependencies are injected through constructors, but a framework could be used to enforce the practice. We could even implement something basic on our own (something like feather). Another nice thing to have IMO would be @CheckForNull/@Nonnull annotations to help static analysis.
  • GitHub actions: further configuration will be needed to run unit tests and static analysis on this code. That's still not part of this PR.

This PR is currently NOT targeting master. It will be merged in the coco attestation feature branch.

GUI diff

No difference.

  • DONE

Documentation

  • Documentation to be discussed

  • DONE

Test coverage

  • Unit tests were added

  • DONE

Links

Issue(s): https://github.com/SUSE/spacewalk/issues/23552

  • DONE

Changelogs

Make sure the changelogs entries you are adding are compliant with https://github.com/uyuni-project/uyuni/wiki/Contributing#changelogs and https://github.com/uyuni-project/uyuni/wiki/Contributing#uyuni-projectuyuni-repository

If you don't need a changelog check, please mark this checkbox:

  • No changelog needed

If you uncheck the checkbox after the PR is created, you will need to re-run changelog_test (see below)

Re-run a test

If you need to re-run a test, please mark the related checkbox, it will be unchecked automatically once it has re-run:

  • Re-run test "changelog_test"
  • Re-run test "backend_unittests_pgsql"
  • Re-run test "java_pgsql_tests"
  • Re-run test "schema_migration_test_pgsql"
  • Re-run test "susemanager_unittests"
  • Re-run test "javascript_lint"
  • Re-run test "spacecmd_unittests"

Before you merge

Check How to branch and merge properly!

@mackdk mackdk requested review from a team as code owners March 8, 2024 11:23
Copy link
Contributor

github-actions bot commented Mar 8, 2024

Suggested tests to cover this Pull Request

Copy link
Contributor

@mcalmer mcalmer left a comment

Choose a reason for hiding this comment

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

just a few points for now

@lucidd lucidd force-pushed the coco-attestation-container branch from 98e9a60 to 0607f1f Compare March 19, 2024 10:19
@lucidd lucidd force-pushed the coco-attestation-container branch 3 times, most recently from 7bc2ed3 to d12bf2e Compare March 19, 2024 14:14
@mackdk mackdk force-pushed the coco-attestation-container branch 2 times, most recently from cc5b9ce to 62a82f5 Compare March 20, 2024 15:31
@mackdk mackdk force-pushed the coco-attestation-container branch from 5d3969c to a156617 Compare March 20, 2024 22:44
Copy link
Member

@rjmateus rjmateus left a comment

Choose a reason for hiding this comment

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

First round about the code structure and maven. So far looks realy good from my point of view. Congrants on all the effort put on this

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.10.0</version>
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this one have <scope>test</scope>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is just the <depedencyManagement> declaration, so I did not include the scope, but yes we can define it directly here as test as well. I will fix it. Same applies to the other 2 entries.

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this one have <scope>test</scope>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.10.0</version>
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this one have <scope>test</scope>

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<scope>test</scope>
Copy link
Member

Choose a reason for hiding this comment

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

Is the log4j needed just for testing? It's not needed at runtime? Is it provided at the runtime?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For compilation of the uyuni-java-common library, only the log4j-api is needed. Of course you will need to provide an implementation at runtime to actually get any logging. Since this is only a common jar, though, I let the decision of that dependency to the consumer of the library. On second thought, I could probably define it as runtime optional.

Copy link
Member

Choose a reason for hiding this comment

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

Or maybe add it as "provided". This way it would be used to compile and test, but not for packaging

</parent>

<artifactId>coco-attestation</artifactId>
<version>5.0.0</version>
Copy link
Member

Choose a reason for hiding this comment

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

This version is specified in here and all the sub-projects. This means if we want to change the version we must update the version number for the next release we would need to change in all places.
Do you think we could apply what is described in here, with revision: https://maven.apache.org/maven-ci-friendly.html

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I actually wanted the version to be defined in a single place, but I probably made something wrong and it wasn't working. I have a look at the link you provided.

%pom_disable_module '../uyuni-java-common'

# Make sure there are not dependencies on the modules projects
%pom_remove_dep 'org.uyuni-project.coco-attestation.module:' attestation-core/pom.xml
Copy link
Member

Choose a reason for hiding this comment

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

Is this line removing the dependency for the core module only or for all?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's only for attestation-core. This lines make sure the maven pom for attestation-core.jar does not declare any dependency on a module implementation. This is because if the pom contains such dependency, the uyuni-coco-attestation package we create in OBS will require the corresponding module package, and we don't want that, as the modules should be pluggable in any combination (it should be the module that requires the core, not the opposite).

On the other hand, during development it's convenient to declare such runtime dependency, otherwise by default you won't have any module on the classpath when you run the app through an IDE. I could probably define a configuration for the maven exec plugin to achieve the same though. I will look into that.

Copy link
Member

@rjmateus rjmateus left a comment

Choose a reason for hiding this comment

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

Just some final comments, but over all I really like the code structure and organization. thank you for the great work


fileSourcesIn.stream()
.flatMap(path -> getConfigurationFiles(path))
.sorted(CONFIG_PATH_COMPARATOR)
Copy link
Member

Choose a reason for hiding this comment

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

I think we are not using this yet, but I don't like this comparator based on the path length.
I think we should not sort the files at all. The last file location should be the last to be loaded and overwrite values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, this is currently not used. It's a re-implementation of the Config class we use in Uyuni and it's almost backwards compatible (a part for a slightly different management of the missing values). Initially I thought we might need to read the same configuration files, hence I implemented this class. In the end, it turned out we didn't need it at all.

Anyway, this is implementing the same sorting logic we currently have, see the Config class for reference.

LOGGER.debug("Adding namespace: {} for file: {}", namespace, file.toAbsolutePath());
for (Object key : properties.keySet()) {
String propertyName = (String) key;
if (!propertyName.startsWith(namespace)) {
Copy link
Member

Choose a reason for hiding this comment

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

what if the property doesn't start with the namespace? Will it get ignored?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not fully sure to understand. This code is meant to ensure all the properties defined in a file named for example rhn_web.conf starts with web.. In case the property does not start in the correct way, the if branch removes it from the set and then reinsert the value with the correct key. Otherwise, if the property has already the correct prefix, it is kept as is.

}

LOGGER.debug("getString() - Getting property: {}", propertyName);
String result = configValues.getProperty(propertyName);
Copy link
Member

Choose a reason for hiding this comment

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

Since we are removing the namespace in here and look only for the property name, can this cause conflicts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If a property is defined without a namespace, it will always override the namespaced one. That's the current behaviour of our configuration logic. Check the test testOverride() in original ConfigTest.

<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<scope>test</scope>
Copy link
Member

Choose a reason for hiding this comment

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

Or maybe add it as "provided". This way it would be used to compile and test, but not for packaging

<artifactId>log4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
Copy link
Member

Choose a reason for hiding this comment

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

Is mybatis used in here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, because the SqlSession object is passed to the module to allow querying the database within the same session where the attestation result row is locked.

We could consider abstracting the MyBatis object behind an interface and hide the implementation details. This would allow the code to not depend on a specific DAO layer. It would essentially mean to rewrite some MyBatis interfaces, and I'm not sure if it's worth.

The search server code does something similar though, by providing the DatabaseManager and Query.

return false;
}

try (var workingDir = directoryProvider.createDirectoryFor(result.getId(), report)) {
Copy link
Member

Choose a reason for hiding this comment

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

Does this directory need to be clean/removed after processing?

Copy link
Contributor Author

@mackdk mackdk Mar 22, 2024

Choose a reason for hiding this comment

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

The cleanup is currently done in the close() method of VerificationDirectory and called automatically by the try-with-resource.

}

private Thread createListenerThread() {
Thread listenerThread = new Thread(this::listenForAttestationResults, "attestation-processor-listener");
Copy link
Member

Choose a reason for hiding this comment

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

In here we are starting a new thread to listen to changes and notify.
What will happen if for any reason this thread dies? Will the program wait forever for a notification and stop processing stuff silently?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the listener thread dies, the main thread should stop as the loop is guarded by the listenerRunning flag which is toggled when the listener stops. Also there is a test case checking that.

I have to expand the coverage of this class though, to verify all the possible edge cases betweem the two threads.

synchronized (dataAvailable) {
dataAvailable.wait();
}
continue;
Copy link
Member

Choose a reason for hiding this comment

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

great trick :)

@mackdk
Copy link
Contributor Author

mackdk commented Mar 26, 2024

I will merge these changes in order to have them available in beta 2. I will address the remaining review comments in a later PR.

@mackdk mackdk merged commit 3f9dcf1 into uyuni-project:coco-attestation-part1 Mar 26, 2024
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants