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

HPCC-28950 JFrog for Manifest/Resource File Management #17674

Merged
merged 1 commit into from
Feb 22, 2024

Conversation

jackdelv
Copy link
Contributor

@jackdelv jackdelv commented Aug 14, 2023

Type of change:

  • This change is a bug fix (non-breaking change which fixes an issue).
  • This change is a new feature (non-breaking change which adds functionality).
  • This change improves the code (refactor or other change that does not change the functionality)
  • This change fixes warnings (the fix does not alter the functionality or the generated code)
  • This change is a breaking change (fix or feature that will cause existing behavior to change).
  • This change alters the query API (existing queries will have to be recompiled)

Checklist:

  • My code follows the code style of this project.
    • My code does not create any new warnings from compiler, build system, or lint.
  • The commit message is properly formatted and free of typos.
    • The commit message title makes sense in a changelog, by itself.
    • The commit is signed.
  • My change requires a change to the documentation.
    • I have updated the documentation accordingly, or...
    • I have created a JIRA ticket to update the documentation.
    • Any new interfaces or exported functions are appropriately commented.
  • I have read the CONTRIBUTORS document.
  • The change has been fully tested:
    • I have added tests to cover my changes.
    • All new and existing tests passed.
    • I have checked that this change does not introduce memory leaks.
    • I have used Valgrind or similar tools to check for potential issues.
  • I have given due consideration to all of the following potential concerns:
    • Scalability
    • Performance
    • Security
    • Thread-safety
    • Cloud-compatibility
    • Premature optimization
    • Existing deployed queries will not be broken
    • This change fixes the problem, not just the symptom
    • The target branch of this pull request is appropriate for such a change.
  • There are no similar instances of the same problem that should be addressed
    • I have addressed them here
    • I have raised JIRA issues to address them separately
  • This is a user interface / front-end modification
    • I have tested my changes in multiple modern browsers
    • The component(s) render as expected

Smoketest:

  • Send notifications about my Pull Request position in Smoketest queue.
  • Test my draft Pull Request.

Testing:

@github-actions
Copy link

@dcamper dcamper requested a review from ghalliday August 14, 2023 16:02
Copy link
Member

@ghalliday ghalliday left a comment

Choose a reason for hiding this comment

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

@jackdelv Don't be put off by the number of comments! Feel free to ask me about any of them.

@@ -198,6 +198,31 @@ void ResourceManifest::loadResource(const char *filepath, MemoryBuffer &content)
read(fio, 0, (size32_t) f->size(), content);
}

void queryJfrog(const char *localpath, IPropertyTree &item)
Copy link
Member

Choose a reason for hiding this comment

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

style: We use query as a prefix for functions that return a value, e.g. queryName().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to callJfrog.

getFileNameOnly(filename, false);
StringBuffer filepath;
StringBuffer machine;
if (repo[0] == '/' && repo[1] == '/')
Copy link
Member

Choose a reason for hiding this comment

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

What are the security implications of this change? As far as I can see it would allow me to overwrite any local file with something stored in a jfrog repository.

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, jfrog would overwrite the file with a resource from artifactory. I have changed it to write to a temporary directory.

if (user && getSecretValue(secretValue.clear(), "jfrog", user, "key", true))
jfrog_cmd.appendf("--user=%s --password=%s ", user, secretValue.trimRight().str());
jfrog_cmd.appendf("\"%s%s\" %s >/dev/null 2>&1", filepath.str(), filename.str(), localpath);
system(jfrog_cmd.str());
Copy link
Member

Choose a reason for hiding this comment

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

Use invoke_program() or runExternalCommand() rather than system.
What happens if the jfrog command fails?

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 am now using runExternalCommand to run the jfrog cli and checking for any errors.

splitFilename(repo, nullptr, &filepath, nullptr, nullptr);
StringBuffer jfrog_cmd("export CI=true; jf rt dl --flat --quiet ");
if (!machine.isEmpty())
jfrog_cmd.appendf("--url=http:%s/artifactory ", machine.str());
Copy link
Member

Choose a reason for hiding this comment

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

http is not secure. Ideally we should be verifying that the server is valid.

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 agree and JFrog has steps for setting up an artifactory repository to work with https, but it is not available on the open source version that I am running of artifactory.

const char *repo = item.queryProp("@repository");
if(repo)
{
StringBuffer filename(localpath);
Copy link
Member

Choose a reason for hiding this comment

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

question: Should localpath be passed in, or should this function be responsible for creating a temporary local path in a valid location? (I don't know what typical requirements would be.)

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 now, I have created a temporary directory under /tmp/HPCCSystems/jfrog_downloads/

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 this should be using

getTempFilePath(tmpDirName, "jfrog", nullptr);

or similar to create the temporary directory.

StringBuffer secretValue;
const char *user = item.queryProp("@user");
if (user && getSecretValue(secretValue.clear(), "jfrog", user, "key", true))
jfrog_cmd.appendf("--user=%s --password=%s ", user, secretValue.trimRight().str());
Copy link
Member

Choose a reason for hiding this comment

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

Passing secrets on the command line isn't very secure. Is there a better alternative way?

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 only other way is to have the user configure jfrog at the command line with jf c add.

@@ -99,6 +99,31 @@ static void loadResource(const char *filepath, MemoryBuffer &content)
read(fio, 0, (size32_t) f->size(), content);
}

static void queryJfrog(const char *localpath, IPropertyTree &item)
Copy link
Member

Choose a reason for hiding this comment

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

A duplicate of the code above? It should only be in one place.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed duplicate code and included hqlmanifest header file.

@@ -223,6 +248,7 @@ void ResourceManifest::addToArchive(IPropertyTree *archive)
IPropertyTree *resTree = additionalFiles->addPropTree("Resource", createPTree("Resource"));
resTree->setProp("@filename", filename);
resTree->setProp("@md5", md5);
queryJfrog(filename, item);
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we want to be downloading from jfrog when we create the archives - otherwise that defeats the object of putting off downloading the resource.

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 am not sure where else to download from jfrog. Wouldn't any other time be too late?

@@ -240,6 +266,7 @@ void ResourceManifest::addToArchive(IPropertyTree *archive)
const char *filepath = item.queryProp("@originalFilename");
resTree->setProp("@originalFilename", filepath);
resTree->setProp("@resourcePath", respath);
queryJfrog(filepath, item);
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't need to write the resource to a known filename - it could read it into the content. Otherwise there is the issue of cleaning up the downloads.

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 am not sure what you mean by this. The only way to access the contents in jfrog artifactory is to download the files.

jfrog_cmd.appendf("--user=%s --password=%s ", user, secretValue.trimRight().str());
jfrog_cmd.appendf("\"%s%s\" %s >/dev/null 2>&1", filepath.str(), filename.str(), localpath);
system(jfrog_cmd.str());
}
Copy link
Member

Choose a reason for hiding this comment

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

We also need an option to verify that the hash for an entry we have downloaded matches a hash specified in the resource.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added option for adding md5 hash of resource in callJfrog.

@ghalliday
Copy link
Member

@jackdelv also note some of the tests are failing. It is worth going back to your PRs after a day to check if they are showing up any problems.

@jackdelv
Copy link
Contributor Author

@ghalliday back to you. I have responded to all of your comments and left a couple of questions.

@jackdelv jackdelv requested a review from ghalliday September 26, 2023 12:03
@jackdelv jackdelv marked this pull request as ready for review October 26, 2023 15:55
@jackdelv jackdelv requested review from ghalliday and removed request for ghalliday October 26, 2023 15:57
Copy link
Member

@ghalliday ghalliday left a comment

Choose a reason for hiding this comment

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

@jackdelv sorry for not re-reviewing this earlier. I have added a few comments, but I think it would be worth scheduling some time to discuss the feature a bit more before responding to them.
I will email the potential users to check exactly what would be useful for this functionality.

@@ -221,9 +267,12 @@ void ResourceManifest::addToArchive(IPropertyTree *archive)
if (!additionalFiles->hasProp(xpath.str()))
{
IPropertyTree *resTree = additionalFiles->addPropTree("Resource", createPTree("Resource"));
StringBuffer filepath(filename);
callJfrog(filepath, absFilename, item);
Copy link
Member

Choose a reason for hiding this comment

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

Clearer to have the if (repo) check outside the call - otherwise when reading this code is poses the question "why is it calling jfrog for a local file.)
Alternatively rename the function to make it clearer what it is doing. E.g.

checkForDownloadFromJfrog()

if the test was outside it would still be better renamed getResourceFromJfrog() or similar

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to getResourceFromJfrog() and moved check outside the call.

StringBuffer userValue;
StringBuffer keyValue;
const char *repoName = item.queryProp("@repoName");
if (repoName && getSecretValue(keyValue, "jfrog", repoName, "key", true) && getSecretValue(userValue, "jfrog", repoName, "user", true))
Copy link
Member

Choose a reason for hiding this comment

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

Is there a more secure way of passing the credentials? Command lines can be read by other processes.

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 user can do a one time configuration of the artifactory connection to avoid passing credentials each time we download from JFrog. I removed the code for passing credentials via the command line.

jfrog_cmd.appendf("--url=http:%s/artifactory ", machine.str());
StringBuffer userValue;
StringBuffer keyValue;
const char *repoName = item.queryProp("@repoName");
Copy link
Member

Choose a reason for hiding this comment

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

repoName isn't very descriptive. How does it relate to repository?

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 intended it to allow for multiple artifactory servers to be used. I removed it because it is no longer needed now that the connection is configured once by the user.

jfrog_cmd.appendf("\"%s%s\" %s", filepath.str(), filename.str(), downloadPath.str());
StringBuffer errOut;
StringBuffer jfrogOut;
if (system("export CI=true") != 0)
Copy link
Member

Choose a reason for hiding this comment

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

I'm surprised this works. The correct way is to use the version of runExternalCommand which allows environment variables to be passed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

throw makeStringExceptionV(0, "Error exporting jfrog setting: export CI=true");
else if(runExternalCommand(errOut, jfrogOut, jfrog_cmd.str(), nullptr) != 0)
throw makeStringExceptionV(0, "Error loading resource from jfrog: \n\"%s\"\n %s", jfrogOut.str(), errOut.str());
const char *md5 = item.queryProp("@md5");
Copy link
Member

Choose a reason for hiding this comment

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

This check would make sense for non-jfrog as well - so I would move out of this function.

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 removed this check because it already occurs outside of the JFrog call.

if (repoName && getSecretValue(keyValue, "jfrog", repoName, "key", true) && getSecretValue(userValue, "jfrog", repoName, "user", true))
jfrog_cmd.appendf("--user=%s --password=%s ", userValue.trimRight().str(), keyValue.trimRight().str());
jfrog_cmd.appendf("\"%s%s\" %s", filepath.str(), filename.str(), downloadPath.str());
StringBuffer errOut;
Copy link
Member

Choose a reason for hiding this comment

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

personally I would add a few newlines to break up the function and make it easier to read. For instance before this line, which is the beginning of the code to execute the command, and line 220 which is the start of the code to build up the command string.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

const char *repo = item.queryProp("@repository");
if(repo)
{
StringBuffer filename(localpath);
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 this should be using

getTempFilePath(tmpDirName, "jfrog", nullptr);

or similar to create the temporary directory.

@jackdelv
Copy link
Contributor Author

jackdelv commented Jan 5, 2024

@ghalliday I addressed all your comments and added some changes. Back to you.

@jackdelv jackdelv requested a review from ghalliday January 5, 2024 16:12
Copy link
Member

@ghalliday ghalliday left a comment

Choose a reason for hiding this comment

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

@jackdelv a few more comments.

@@ -1,3 +1,3 @@
<Manifest>
<Resource type='jar' filename='javaembed_ex7.jar'/>
<Resource type='jar' filename='javaembed_ex7.jar' repository='/generic-local/javaFiles/' md5='d933045e7fed10ff9ccddfc9743b0538'/>
Copy link
Member

Choose a reason for hiding this comment

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

I assume this is for testing and it should be reverted before merging.

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, that will be reverted.

getTempFilePath(downloadPath, "jfrog", nullptr);
downloadPath.append(filename);

StringBuffer jfrogCmd("jf rt dl --flat --quiet ");
Copy link
Member

Choose a reason for hiding this comment

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

How is authentication handled?

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 server address and credentials are configured with the JFrog cli before making any calls to Artifactory. The connections from getResourceFromJfrog use the new configuration instead of having to pass the credentials at the command line.

@@ -221,9 +242,13 @@ void ResourceManifest::addToArchive(IPropertyTree *archive)
if (!additionalFiles->hasProp(xpath.str()))
{
IPropertyTree *resTree = additionalFiles->addPropTree("Resource", createPTree("Resource"));
StringBuffer filepath(filename);
if (item.queryProp("@repository"))
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 this is a case where you do not want to fetch the artefact at this point. This is before the query is sent to the system - so it would be downloaded by the user, added to the archive and then uploaded. The aim of this change is to avoid that step.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I think I understand. I am just not sure where the resource is used after being added to the archive.

@jackdelv jackdelv requested a review from ghalliday January 9, 2024 14:57
@jackdelv jackdelv force-pushed the JFrog-dev branch 2 times, most recently from 6b30f8a to e0673a8 Compare January 16, 2024 15:27
@jackdelv
Copy link
Contributor Author

@ghalliday I moved the call to getResourceFromJfrog to when the resource is fetch from the archive rather than storing it in the archive.

@ghalliday
Copy link
Member

Let's discuss this verbally. We missed out clearly talking through the requirements first - we need to do that and then see where we stand.

@jackdelv
Copy link
Contributor Author

@ghalliday Authentication for Jfrog is now set up using the secrets vault. A Jfrog config file is created rather than passing the credentials through the command line.

Copy link
Member

@ghalliday ghalliday left a comment

Choose a reason for hiding this comment

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

@jackdelv This looks close. A few final comments.

Also a general comment - it is better not to rebase your branch unless you need to because it makes it harder to track which changes have taken place since the last review.

Also please can you update the jira to contain a summary of what has been implemented - e.g. the format and options in the resource definition to use the feature.

It will also need some changes to ensure the secrets are accessible from containerized builds, but I will add a separate comment for that,

@@ -38,7 +38,8 @@ include_directories (
./../../rtl/eclrtl
./../../system/include
./../../dali/base
./../../system/jlib
./../../system/jlib
./../../ecl/hql
Copy link
Member

Choose a reason for hiding this comment

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

This dll shouldn't really have a dependency on hql.
I think the cleanest solution is to move getResourceFromJfrog into jlib. Either into a new file, or added to something like jutil.hpp/cpp

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved getResourceFromJfrog to jutil.


void getResourceFromJfrog(StringBuffer &localPath, IPropertyTree &item)
{
addSecretToJfrog(item);
Copy link
Member

Choose a reason for hiding this comment

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

security: Ensure this file is deleted when the command has executed (for success or failure)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

],\n\
\"version\": \"6\"\n}\n", server, server, server, server, server, server, user, password.str());

StringBuffer configPath(getenv("HOME"));
Copy link
Member

Choose a reason for hiding this comment

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

Use getHomeDir() in jmisc.hpp - which will also work on windows.
I assume this is a location defined by jfrog.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

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, this is the location where the JFrog CLI expects the config file to be if there are no credentials passed through the command line.

{
StringBuffer password;
const char *user = item.queryProp("@jfrogUser");
getSecretValue(password, "jfrog", user, "password", true);
Copy link
Member

Choose a reason for hiding this comment

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

@afishbeck Do you think this is reasonable to add a new category to access jfrog secrets? Are we going to continue to have new secret categories?
Would it be better to have an "external" category with a key of "jfrog:"? What other alternatives are there. The only other similar example at the moment is "git", but maybe we will end up with docker, or other examples.
Thoughts?

@jackdelv jackdelv requested a review from ghalliday February 16, 2024 15:57
@ghalliday
Copy link
Member

@jackdelv great, it looks like it is ready to merge. Please can you

  • squash all the commits
  • Add details into the jira about how it is used - what the options for the manifest are, how the secrets are configured etc.
  • Create a documentation jira and link to this one.
  • Talk to @cloLN about adding a test using the opinionated cluster to make sure it works end to end on a container system.

Actually... we still need to think about making the secrets available in container builds from the helm files. I will ask Tony exactly what he thinks, but I think worth squashing all changes so far.

@jackdelv
Copy link
Contributor Author

@ghalliday I added a description to this JIRA detailing the options and adding the secrets. If I left anything out please let me know. Here is the documentation JIRA: https://track.hpccsystems.com/browse/HPCC-31310. Back to you.

@jackdelv jackdelv requested a review from ghalliday February 20, 2024 13:43
@ghalliday
Copy link
Member

I think the final change is to update the helm files to ensure the secrets are available in the cloud version. Two places need to change (the same place that "git" secrets are currently supported):
issuers.yaml: line 139 ($categories :=) add "jfrog" to the list
eclccserver.yaml: line 127 ($secretsCategories :=...) . add jfrog to the list.
Please feel free to make those changes and squash them at the same time.

@jackdelv
Copy link
Contributor Author

@ghalliday Squashed.

@ghalliday ghalliday merged commit 4aa1362 into hpcc-systems:master Feb 22, 2024
49 of 51 checks passed
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