Skip to content

Conversation

@jantman
Copy link
Contributor

@jantman jantman commented Dec 18, 2020

This PR is an initial, proof-of-concept implementation of a shell script hook plugin for TerraformEnvironmentStage, as described in #250. The plugin name (TerraformEnvironmentStageShellHookPlugin) is intended to be an indication that this is not an attempt at an all-encompassing solution to the hook problem, and is one initial implementation that may or may not inspire something at a higher level.

The code included here has full unit test coverage, and has also been tested in a Jenkinsfile on a private Jenkins instance. All seems to be working well.

@jantman
Copy link
Contributor Author

jantman commented Dec 18, 2020

The TravisCI tests are failing with:

Execution failed for task ':compileTestGroovy'.

> BUG! exception in phase 'class generation' in source unit '/home/travis/build/manheim/terraform-pipeline/test/HookPointTest.groovy' unsupported Target MODULE

I have no idea what's causing this. ./gradlew check --info succeeds locally for me in my working clone. In a fresh clone, it seems to be failing with an unrelated order-dependent error ( looks like a resurgence of the same bug that #313 sought to fix).

(PLAN_COMMAND): new HookPoint(PLAN_COMMAND),
(APPLY): new HookPoint(APPLY),
(APPLY_COMMAND): new HookPoint(APPLY_COMMAND)
]
Copy link
Collaborator

@kmanning kmanning Dec 23, 2020

Choose a reason for hiding this comment

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

Would it be easier if this were initialized as an empty map? You're iterating over all the hooks to check if the shells have been set for the hooks. If you had the map empty, and only initialized the map when a hook point were added for the given key (ALL/INIT/PLAN, etc...) - you'd only be iterating over the hooks that actually have values.

I think it might make both your testing and implementation simpler. <-- that's a maybe. It might not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On the flip side, it means that withHook() would need to null-check each of the values, and construct the new HookPoint objects if they're needed. Maybe this is backwards, but I wanted to contain all of the logic of "is this hook set" inside the HookPoint class, which led to the current implementation. Though I'm certainly open to changing it if you prefer.

Copy link
Collaborator

Choose a reason for hiding this comment

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

To be more specific, I think there's a lot of examples like this, where you're iterating over all the hooks because they're all non-null. Looping can be tricky in a test - there's actually no guarantee that a PLAN_COMMAND entry will even exist in the list.

        @Test
        void setRunBefore() {
            TerraformEnvironmentStageShellHookPlugin.withHook(PLAN_COMMAND, 'foo bar', WhenToRun.BEFORE)
            def hooks = TerraformEnvironmentStageShellHookPlugin.hooks
            hookKeys.each {
                if (it == PLAN_COMMAND) {
                    assertTrue(hooks[it].isConfigured())
                    assertEquals(hooks[it].runBefore, 'foo bar')
                    assertNull(hooks[it].runAfterAlways)
                    assertNull(hooks[it].runAfterOnFailure)
                    assertNull(hooks[it].runAfterOnSuccess)
                } else {
                    assertFalse(hooks[it].isConfigured())
                }
            }
        }

I was wondering if it might be more straight forward to do something like this, which obviates the need to loop, and by passes the problem of the element not being in the list.

        @Test
        void setRunBefore() {
            TerraformEnvironmentStageShellHookPlugin.withHook(PLAN_COMMAND, 'foo bar', WhenToRun.BEFORE)
            def hooks = TerraformEnvironmentStageShellHookPlugin.hooks
            assertEquals(1, hooks.size())
            
            def planHooks = hooks[PLAN_COMMAND]
            assertTrue(planHooks.isConfigured())
            assertEquals(planHooks.runBefore, 'foo bar')
            assertNull(planHooks.runAfterAlways)
            assertNull(planHooks.runAfterOnFailure)
            assertNull(planHooks.runAfterOnSuccess)
        }

Which then suggests even more simplification with:

        @Test
        void setRunBefore() {
            TerraformEnvironmentStageShellHookPlugin.withHook(PLAN_COMMAND, 'foo bar', WhenToRun.BEFORE)
            def planHooks = TerraformEnvironmentStageShellHookPlugin.getHooks(PLAN_COMMAND)
            assertTrue(planHooks.isConfigured())
            assertEquals(planHooks.runBefore, 'foo bar')
            assertNull(planHooks.runAfterAlways)
            assertNull(planHooks.runAfterOnFailure)
            assertNull(planHooks.runAfterOnSuccess)
        }

It's a suggestion though, just based on looking at the tests, and not looking at the implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So... those alternatives are missing the intended point of the test, or half of it.

        @Test
        void setRunBefore() {
            TerraformEnvironmentStageShellHookPlugin.withHook(PLAN_COMMAND, 'foo bar', WhenToRun.BEFORE)
            def hooks = TerraformEnvironmentStageShellHookPlugin.hooks
            hookKeys.each {
                if (it == PLAN_COMMAND) {
                    assertTrue(hooks[it].isConfigured())
                    assertEquals(hooks[it].runBefore, 'foo bar')
                    assertNull(hooks[it].runAfterAlways)
                    assertNull(hooks[it].runAfterOnFailure)
                    assertNull(hooks[it].runAfterOnSuccess)
                } else {
                    assertFalse(hooks[it].isConfigured())
                }
            }
        }

Is intended to test that the plan before hook, and ONLY the plan before hook, has been set. The iteration is there to ensure that none of the other hooks, even though they're present, have been touched. If there was an easy way to serialize the complete state of hooks and test against that, then I'd do it.

The iteration here was just intended to simplify:

assertFalse(TerraformEnvironmentStageShellHookPlugin.getHooks(INIT_COMMAND).isConfigured())
assertFalse(TerraformEnvironmentStageShellHookPlugin.getHooks(PLAN).isConfigured())
def planHooks = TerraformEnvironmentStageShellHookPlugin.getHooks(PLAN_COMMAND)
assertTrue(planHooks.isConfigured())
assertEquals(planHooks.runBefore, 'foo bar')
assertNull(planHooks.runAfterAlways)
assertNull(planHooks.runAfterOnFailure)
assertNull(planHooks.runAfterOnSuccess)
assertFalse(TerraformEnvironmentStageShellHookPlugin.getHooks(APPLY).isConfigured())
assertFalse(TerraformEnvironmentStageShellHookPlugin.getHooks(APPLY_COMMAND).isConfigured())

On another note:

there's actually no guarantee that a PLAN_COMMAND entry will even exist in the list.

Why not? Am I missing something?

Copy link
Collaborator

@kmanning kmanning Jan 4, 2021

Choose a reason for hiding this comment

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

The iteration is there to ensure that none of the other hooks, even though they're present, have been touched

That's where I was suggesting that using nulls might be helpful to simply that intent.

            def hooks = TerraformEnvironmentStageShellHookPlugin.hooks
            assertEquals(1, hooks.size())
            ...
            def planHooks = hooks[PLAN_COMMAND]
            ...

The existence of a single non-null hook confirms that all other hooks are not set. No worries though, it was just a thought.

Is intended to test that the plan before hook, and ONLY the plan before hook, has been set.
On another note:

there's actually no guarantee that a PLAN_COMMAND entry will even exist in the list.

Why not? Am I missing something?

Apparently, I missed something, and not you. I didn't realize until now that you created a hookKeys array, and are iterating over THAT, and not iterating over the hooks - so I'm wrong.

class TerraformEnvironmentStageShellHookPluginTest {
    def hookKeys = [ALL, INIT_COMMAND, PLAN, PLAN_COMMAND, APPLY, APPLY_COMMAND]
    ...
            def hooks = TerraformEnvironmentStageShellHookPlugin.hooks
       -->  hookKeys.each {
                if (it == PLAN_COMMAND) {
                    assertTrue(hooks[it].isConfigured())
                    assertEquals(hooks[it].runBefore, 'foo bar')
                    assertNull(hooks[it].runAfterAlways)
                    assertNull(hooks[it].runAfterOnFailure)
                    assertNull(hooks[it].runAfterOnSuccess)
                } else {
                    assertFalse(hooks[it].isConfigured())
                }
            }

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 existence of a single non-null hook confirms that all other hooks are not set. No worries though, it was just a thought.

Ah, ok, got it. So part of my concern was that since we're just using string keys here (albeit static final string variables instead of using the strings directly), to ensure that only the correct hook was configured, even if others existed as non-null. Granted, I suppose that's only a concern with the specific current implementation.

Apparently, I missed something, and not you. I didn't realize until now that you created a hookKeys array, and are iterating over THAT, and not iterating over the hooks - so I'm wrong.

Ah, ok, yeah.

@kmanning
Copy link
Collaborator

The TravisCI tests are failing with:

Execution failed for task ':compileTestGroovy'.

> BUG! exception in phase 'class generation' in source unit '/home/travis/build/manheim/terraform-pipeline/test/HookPointTest.groovy' unsupported Target MODULE

I have no idea what's causing this. ./gradlew check --info succeeds locally for me in my working clone. In a fresh clone, it seems to be failing with an unrelated order-dependent error ( looks like a resurgence of the same bug that #313 sought to fix).

I don't think that's related to issue #313.

I can't reproduce locally, but I did find the same error referenced elsewhere.
The thread seems to suggest it's a problem with a mismatch between java/gradle versions.
It's unclear why your PR would trigger this - master continues to run find with the same combination.

https://stackoverflow.com/questions/45899027/errorbug-exception-in-phase-class-generation-in-source-unit-buildscript

@kmanning
Copy link
Collaborator

kmanning commented Dec 23, 2020

Looks like JobDSL 1.77 upgraded groovy to 2.4.12. The CHANGELOG for 2.4.12 notes a fix for compile errors in legacy version.

Upgrading groovy seems to make the problem go away. Upgrading gradle reveals the problem locally (which doesn't make sense, since travisCI is using ./gradlew - same as what we'd be using locally).

At any rate, I'll cut a difficult issue to address this. See Issue #321

@kmanning
Copy link
Collaborator

PR for Issue #321 is available (See #322) - I can merge the change into master, and you can rebase to fix your PR.

@kmanning kmanning added this to the v5.13 milestone Dec 23, 2020
@kmanning
Copy link
Collaborator

Also, can you update the CHANGELOG for this PR?

@jantman
Copy link
Contributor Author

jantman commented Jan 4, 2021

@kmanning I've updated the changelog as requested. Once #322 is merged to master I'll rebase. Still have a few discussion items pending from your review.

@kmanning
Copy link
Collaborator

kmanning commented Jan 4, 2021

@jantman Merged

@jantman
Copy link
Contributor Author

jantman commented Jan 4, 2021

@kmanning Rebased and tests passing. Thanks!!

@codecov-io
Copy link

codecov-io commented Jan 5, 2021

Codecov Report

Merging #319 (8b22c07) into master (76829ed) will increase coverage by 0.07%.
The diff coverage is 80.95%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #319      +/-   ##
==========================================
+ Coverage   79.06%   79.13%   +0.07%     
==========================================
  Files          41       43       +2     
  Lines        1065     1107      +42     
  Branches      247      262      +15     
==========================================
+ Hits          842      876      +34     
  Misses         99       99              
- Partials      124      132       +8     
Impacted Files Coverage Δ
src/HookPoint.groovy 70.00% <70.00%> (ø)
...rc/TerraformEnvironmentStageShellHookPlugin.groovy 84.37% <84.37%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 76829ed...8b22c07. Read the comment docs.

@kmanning kmanning linked an issue Jan 5, 2021 that may be closed by this pull request
@kmanning kmanning merged commit b3abb35 into manheim:master Jan 5, 2021
@jantman jantman deleted the issues/250 branch January 5, 2021 14:49
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.

Provide a hook for arbitrary scripts after 'apply'

3 participants