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

internal/pkg/scorecard: implement plugin system #1379

Merged
merged 15 commits into from
May 15, 2019

Conversation

AlexNPavel
Copy link
Contributor

Description of the change: Implement scorecard plugins

Motivation for the change: This PR implements support for scorecard plugins as described in this proposal: https://github.com/operator-framework/operator-sdk/blob/master/doc/proposals/scorecard-plugin-system.md

The Basic and OLM test suites will be split out into separate plugins after this PR is merged.

@AlexNPavel AlexNPavel added kind/feature Categorizes issue or PR as related to a new feature. scorecard Issue relates to the scorecard subcomponent labels May 2, 2019
@openshift-ci-robot openshift-ci-robot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label May 2, 2019
Copy link
Member

@shawn-hurley shawn-hurley left a comment

Choose a reason for hiding this comment

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

LGTM, I would suggest completing the helper function method in the TODO

internal/pkg/scorecard/helpers.go Outdated Show resolved Hide resolved
internal/pkg/scorecard/scorecard.go Outdated Show resolved Hide resolved
internal/pkg/scorecard/scorecard.go Outdated Show resolved Hide resolved
internal/pkg/scorecard/scorecard.go Outdated Show resolved Hide resolved
}
for _, file := range files {
cmd := exec.Command("bash", "-c", "./bin/"+file.Name())
output, err := cmd.CombinedOutput()
Copy link
Member

Choose a reason for hiding this comment

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

In this case, it might make sense to capture stdout and stderr separately. We'd parse the stdout as JSON and maybe include the stderr in a log?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Where exactly would we put the log? We provide a per-suite log field for plugins to use so they shouldn't be outputting anything to stdout or stderr except the JSON or potentially fatal errors. I assume we could add it into the main scorecard log (in the final ScorecardOutput), but that could get messy pretty quickly

Copy link
Member

Choose a reason for hiding this comment

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

Yeah that's a good question. Wdyt of checking if result.Log is empty. If so, set result.Log to the stderr buffer. If not, log a warning that would be seen in the human-readable output?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That sounds reasonable. I'll do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, there's a problem with that. A single plugin can have more than 1 suite. If that's the case, we won't be able to simply add the stderr to a log field.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The way I'll handle this for now is using log.Warn for the stderr output, which will print it nicely in the human-readable output or add it to the main log for the json output.

for _, suite := range suites {
for idx, res := range suite.TestResults {
suite.TestResults[idx] = UpdateState(res)
for _, suite := range pluginOutputs {
Copy link
Contributor

Choose a reason for hiding this comment

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

A for inside another shows not be the best solution. Note the Big O here. Could it not cause performance issues? Could it not be improved to achieve a better performance?

numSuites := 0
for _, plugin := range pluginOutputs {
for _, suite := range plugin.Results {
fmt.Printf("%s:\n", suite.Name)
Copy link
Contributor

Choose a reason for hiding this comment

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

The same point of attention in the impl. The complexity of this impl.

for _, suite := range plugin.Results {
for _, result := range suite.Tests {
for _, suggestion := range result.Suggestions {
// 33 is yellow (specifically, the same shade of yellow that logrus uses for warnings)
Copy link
Contributor

Choose a reason for hiding this comment

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

A loop the inside of another loop 4 times. Is not possible to solve this need in a more performative way?

for _, err := range result.Errors {
// 31 is red (specifically, the same shade of red that logrus uses for errors)
fmt.Printf("\x1b[%dmERROR:\x1b[0m %s\n", 31, err)
for _, plugin := range pluginOutputs {
Copy link
Contributor

Choose a reason for hiding this comment

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

A loop the inside of another loop 4 times. Is not possible to solve this need in a more performative way?

@AlexNPavel
Copy link
Contributor Author

@camilamacedo86 BigO for these loops is irrelevant to the runtime of the scorecard. Much of the time spent in the scorecard will be on the actual plugins themselves (which includes the Basic and OLM tests), which will likely be waiting on networking. The plugins should be optimized if they are compute heavy. However, the time spent in the loops you mentioned will be on the order of milliseconds (excluding the print syscall time, which we would still have) even for runs with a lot of output and plugins. Those extra couple of milliseconds spent in the loops are irrelevant to the overall runtime of the scorecard and any optimizations there would sacrifice clarity.

@camilamacedo86
Copy link
Contributor

camilamacedo86 commented May 8, 2019

@AlexNPavel

IMHO shows that it could be changed/improved. Now it shows irrelevant, however, in it could grow and the issues started to be faced. The CI tests take too long already. Also, could not the code be more understandable and in this way easier to keep maintained and be improved without these loops inside of loops?

Note that the following exactly the same loop inside of a loop is made many times.

for _, plugin := range pluginOutputs {
			for _, suite := range plugin.Results {

You make a great work here, it is just a suggestion. Please, feel free to move forward as you decide

@shawn-hurley
Copy link
Member

You make a great work here, it is just a suggestion. Please, feel free to move forward as you decide

I think that if we want to make this better with fewer loops we would need to redesign the structs.

I think that something we could do in this PR or as a follow up to make this better is:

  1. Have the Plugin struct responsible for its output.
  2. Allow this function to take a channel of type string. Allow the function to do the loops and create its output and then return it to the channel.
  3. For each plugin calls each of these and then takes the return strings and create the output.

If that is too much, at the very least lets combine the two different for loops, by creating the output in two string buffers and then combining.

for _, suite := range plugin.Results {
				for _, result := range suite.Tests {
					for _, err := range result.Errors {
						// 31 is red (specifically, the same shade of red that logrus uses for errors)
						errorBuffer.Add(fmt.Sprintf("\x1b[%dmERROR:\x1b[0m %s\n", 31, err))
					}
					for _, suggestion := range result.Suggestions {
						// 33 is yellow (specifically, the same shade of yellow that logrus uses for warnings)
						suggestionBuffer.Addfmt.Sprintf("\x1b[%dmSUGGESTION:\x1b[0m %s\n", 33, suggestion))
					}

				}
}

/cc @camilamacedo86 @AlexNPavel

BTW I personally feel this is acceptable to do in a follow-up.

@openshift-ci-robot
Copy link

@shawn-hurley: GitHub didn't allow me to request PR reviews from the following users: camilamacedo86, AlexNPavel.

Note that only operator-framework members and repo collaborators can review this PR, and authors cannot review their own PRs.

In response to this:

You make a great work here, it is just a suggestion. Please, feel free to move forward as you decide

I think that if we want to make this better with fewer loops we would need to redesign the structs.

I think that something we could do in this PR or as a follow up to make this better is:

  1. Have the Plugin struct responsible for its output.
  2. Allow this function to take a channel of type string. Allow the function to do the loops and create its output and then return it to the channel.
  3. For each plugin calls each of these and then takes the return strings and create the output.

If that is too much, at the very least lets combine the two different for loops, by creating the output in two string buffers and then combining.

for _, suite := range plugin.Results {
  			for _, result := range suite.Tests {
  				for _, err := range result.Errors {
  					// 31 is red (specifically, the same shade of red that logrus uses for errors)
  					errorBuffer.Add(fmt.Sprintf("\x1b[%dmERROR:\x1b[0m %s\n", 31, err))
  				}
  				for _, suggestion := range result.Suggestions {
  					// 33 is yellow (specifically, the same shade of yellow that logrus uses for warnings)
  					suggestionBuffer.Addfmt.Sprintf("\x1b[%dmSUGGESTION:\x1b[0m %s\n", 33, suggestion))
  				}

  			}
}

/cc @camilamacedo86 @AlexNPavel

BTW I personally feel this is acceptable to do in a follow-up.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

Copy link
Member

@joelanford joelanford left a comment

Choose a reason for hiding this comment

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

👍 on @shawn-hurley's comment about further improvements. I also think we could address them in a follow-on PR.

A few more nits and comments.

internal/pkg/scorecard/scorecard.go Outdated Show resolved Hide resolved
internal/pkg/scorecard/scorecard.go Outdated Show resolved Hide resolved
@@ -147,7 +135,7 @@ func TestResultToScorecardTestResult(tr TestResult) scapiv1alpha1.ScorecardTestR
}

// UpdateState updates the state of a TestResult.
func UpdateState(res TestResult) TestResult {
func UpdateState(res scapiv1alpha1.ScorecardTestResult) scapiv1alpha1.ScorecardTestResult {
Copy link
Member

Choose a reason for hiding this comment

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

We have a handful of exported helper functions for the scapiv1alpha1 types in this package. I'd suggest moving them into the scapiv1alpha1 package, possibly making them methods on the scapiv1alpha1 structs (where that makes sense), and also possibly unexporting them if we think they'll only be used on the SDK side of the plugin interface.

This could be in a follow-on PR. Since we're going to reorganize our built-in scorecard tests as plugins, I'd be interested in your thoughts on a design for a plugin library that would make it easy to write a plugin in go.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I'll make a PR to expose some of these. I was initially intending to expose some of this with the JSON PR, but it wasn't quite ready. Now that we have the plugin support in place, we can probably expose the helpers.

Copy link
Member

@joelanford joelanford left a comment

Choose a reason for hiding this comment

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

LGTM

suite.PartialPass++
case scapiv1alpha1.FailState:
suite.Fail++
}
Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably have a default case to catch and log any unknown state.
Even though it doesn't seem like we'll end up in an unknown state, it's still good practice in case we change it in the future.

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'll do this in a future PR. The issue is that there is no way to really handle this cleanly right now. One way to fix this would be to add another field to the SuiteResults of invalid. That way we can indicate to users that a plugin returned an invalid state. Another option (which is what I want to do in a future PR) is to change the state from whatever incorrect state it was in to error and add a string to the errors array indicating that the scorecard marked this test as failed due to an invalid state.

Technically though, due to the way the helpers are set up (we calculate the state using the earned vs max points unless the state is error), the only situation where this could occur (currently) would be if the earned points are higher than the max. I'd be fine marking that as an error.

@hasbro17
Copy link
Contributor

Looks good overall. Just some small nits.

Copy link
Contributor

@hasbro17 hasbro17 left a comment

Choose a reason for hiding this comment

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

One last nit: We should update the CLI reference guide for the flags. But you can leave that out unless you have a separate PR for doc changes.

@AlexNPavel
Copy link
Contributor Author

@hasbro17 I am going to make a PR with all the doc changes today

@AlexNPavel AlexNPavel merged commit aa1918a into operator-framework:master May 15, 2019
@AlexNPavel AlexNPavel deleted the sc-plugin-support branch May 15, 2019 21:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/feature Categorizes issue or PR as related to a new feature. scorecard Issue relates to the scorecard subcomponent size/L Denotes a PR that changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants